codebyplan 1.10.3 → 1.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/dist/cli.js +534 -209
- package/package.json +1 -1
- package/templates/agents/cbp-round-executor.md +1 -1
- package/templates/agents/cbp-task-check.md +0 -1
- package/templates/agents/cbp-test-e2e-agent.md +2 -2
- package/templates/agents/cbp-testing-qa-agent.md +7 -29
- package/templates/hooks/README.md +58 -16
- package/templates/hooks/cbp-statusline.mjs +385 -0
- package/templates/hooks/cbp-statusline.py +331 -0
- package/templates/hooks/cbp-statusline.sh +138 -82
- package/templates/hooks/cbp-subagent-statusline.mjs +200 -0
- package/templates/hooks/cbp-subagent-statusline.py +183 -0
- package/templates/hooks/cbp-subagent-statusline.sh +87 -39
- package/templates/skills/cbp-checkpoint-complete/SKILL.md +1 -1
- package/templates/skills/cbp-frontend-ui/SKILL.md +2 -2
- package/templates/skills/cbp-round-end/SKILL.md +16 -26
- package/templates/skills/cbp-round-execute/SKILL.md +2 -2
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# @hook: NOT-A-HOOK (subagentStatusLine renderer, invoked via the cbp-subagent-statusline.sh dispatcher)
|
|
3
|
+
# Claude Code Subagent Status Line — python renderer. Byte-identical output to the
|
|
4
|
+
# bash renderer in cbp-subagent-statusline.sh and the node renderer in
|
|
5
|
+
# cbp-subagent-statusline.mjs. Output protocol: one {"id":...,"content":...} JSON
|
|
6
|
+
# line per task. See the .sh file for the full option/env/seam contract.
|
|
7
|
+
# Never throws to stdout.
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import math
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main():
|
|
17
|
+
raw_input = sys.stdin.read()
|
|
18
|
+
try:
|
|
19
|
+
data = json.loads(raw_input)
|
|
20
|
+
except Exception:
|
|
21
|
+
data = {}
|
|
22
|
+
if not isinstance(data, dict):
|
|
23
|
+
data = {}
|
|
24
|
+
|
|
25
|
+
root = os.environ.get("CBP_STATUSLINE_ROOT") or (sys.argv[1] if len(sys.argv) > 1 else "")
|
|
26
|
+
if not root:
|
|
27
|
+
root = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."))
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
columns = math.trunc(float(data.get("columns") or 0))
|
|
31
|
+
except (TypeError, ValueError):
|
|
32
|
+
columns = 0
|
|
33
|
+
tasks = data.get("tasks")
|
|
34
|
+
if not isinstance(tasks, list):
|
|
35
|
+
tasks = []
|
|
36
|
+
|
|
37
|
+
# ---- Config: no_color from statusline.json -------------------------------
|
|
38
|
+
cfg_no_color = False
|
|
39
|
+
try:
|
|
40
|
+
with open(os.path.join(root, ".codebyplan", "statusline.json"), "r", encoding="utf-8") as fh:
|
|
41
|
+
parsed = json.load(fh)
|
|
42
|
+
if isinstance(parsed, dict) and isinstance(parsed.get("no_color"), bool):
|
|
43
|
+
cfg_no_color = parsed["no_color"]
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
no_color = (
|
|
48
|
+
(os.environ.get("NO_COLOR") not in (None, ""))
|
|
49
|
+
or os.environ.get("CBP_SUBAGENT_STATUSLINE_NO_COLOR") == "1"
|
|
50
|
+
or cfg_no_color is True
|
|
51
|
+
)
|
|
52
|
+
if no_color:
|
|
53
|
+
RST = DIM = BOLD = GREEN = RED = BLUE = ""
|
|
54
|
+
else:
|
|
55
|
+
RST = "\x1b[0m"
|
|
56
|
+
DIM = "\x1b[2m"
|
|
57
|
+
BOLD = "\x1b[1m"
|
|
58
|
+
GREEN = "\x1b[32m"
|
|
59
|
+
RED = "\x1b[31m"
|
|
60
|
+
BLUE = "\x1b[34m"
|
|
61
|
+
|
|
62
|
+
def fmt_k(val):
|
|
63
|
+
v = float(val)
|
|
64
|
+
if v >= 1000000:
|
|
65
|
+
t = math.floor((v + 50000) / 100000)
|
|
66
|
+
return "%d.%dM" % (t // 10, t % 10)
|
|
67
|
+
elif v >= 1000:
|
|
68
|
+
t = math.floor((v + 50) / 100)
|
|
69
|
+
return "%d.%dK" % (t // 10, t % 10)
|
|
70
|
+
return str(int(v))
|
|
71
|
+
|
|
72
|
+
def cbp_now():
|
|
73
|
+
nv = os.environ.get("CBP_STATUSLINE_NOW")
|
|
74
|
+
if nv not in (None, ""):
|
|
75
|
+
return math.trunc(float(nv))
|
|
76
|
+
return math.floor(time.time())
|
|
77
|
+
|
|
78
|
+
def fmt_age_secs(start):
|
|
79
|
+
delta = cbp_now() - math.trunc(float(start))
|
|
80
|
+
if delta < 0:
|
|
81
|
+
delta = 0
|
|
82
|
+
if delta >= 3600:
|
|
83
|
+
return "%dh%dm" % (delta // 3600, (delta % 3600) // 60)
|
|
84
|
+
if delta >= 60:
|
|
85
|
+
return "%dm%ds" % (delta // 60, delta % 60)
|
|
86
|
+
return "%ds" % delta
|
|
87
|
+
|
|
88
|
+
def is_ascii_letter(b):
|
|
89
|
+
return (0x41 <= b <= 0x5A) or (0x61 <= b <= 0x7A)
|
|
90
|
+
|
|
91
|
+
def truncate(rendered):
|
|
92
|
+
# ANSI-aware byte-based truncation — mirrors the bash byte walk exactly.
|
|
93
|
+
if not (columns > 0):
|
|
94
|
+
return rendered
|
|
95
|
+
buf = rendered.encode("utf-8")
|
|
96
|
+
raw_len = len(buf)
|
|
97
|
+
target = columns - 3
|
|
98
|
+
if target < 0:
|
|
99
|
+
target = 0
|
|
100
|
+
vis = 0
|
|
101
|
+
i = 0
|
|
102
|
+
in_esc = False
|
|
103
|
+
while i < raw_len and vis < target:
|
|
104
|
+
b = buf[i]
|
|
105
|
+
if in_esc:
|
|
106
|
+
if is_ascii_letter(b):
|
|
107
|
+
in_esc = False
|
|
108
|
+
elif b == 0x1B:
|
|
109
|
+
in_esc = True
|
|
110
|
+
else:
|
|
111
|
+
vis += 1
|
|
112
|
+
i += 1
|
|
113
|
+
while i < raw_len:
|
|
114
|
+
b = buf[i]
|
|
115
|
+
if in_esc:
|
|
116
|
+
if is_ascii_letter(b):
|
|
117
|
+
in_esc = False
|
|
118
|
+
i += 1
|
|
119
|
+
elif b == 0x1B:
|
|
120
|
+
in_esc = True
|
|
121
|
+
i += 1
|
|
122
|
+
else:
|
|
123
|
+
break
|
|
124
|
+
if i < raw_len:
|
|
125
|
+
head = buf[:i]
|
|
126
|
+
tail = (RST + "...").encode("utf-8")
|
|
127
|
+
return (head + tail).decode("utf-8", errors="replace")
|
|
128
|
+
return rendered
|
|
129
|
+
|
|
130
|
+
out = []
|
|
131
|
+
for task in tasks:
|
|
132
|
+
if not isinstance(task, dict):
|
|
133
|
+
continue
|
|
134
|
+
t_id = "" if task.get("id") is None else str(task.get("id"))
|
|
135
|
+
if t_id == "":
|
|
136
|
+
continue
|
|
137
|
+
t_name = "" if task.get("name") is None else str(task.get("name"))
|
|
138
|
+
t_type = "" if task.get("type") is None else str(task.get("type"))
|
|
139
|
+
t_status = "" if task.get("status") is None else str(task.get("status"))
|
|
140
|
+
t_desc = "" if task.get("description") is None else str(task.get("description"))
|
|
141
|
+
try:
|
|
142
|
+
t_start = math.trunc(float(task.get("startTime") or 0))
|
|
143
|
+
except (TypeError, ValueError):
|
|
144
|
+
t_start = 0
|
|
145
|
+
try:
|
|
146
|
+
t_tokens = math.trunc(float(task.get("tokenCount") or 0))
|
|
147
|
+
except (TypeError, ValueError):
|
|
148
|
+
t_tokens = 0
|
|
149
|
+
|
|
150
|
+
if t_status == "running":
|
|
151
|
+
icon = "%s●%s" % (GREEN, RST)
|
|
152
|
+
elif t_status == "pending":
|
|
153
|
+
icon = "%s○%s" % (DIM, RST)
|
|
154
|
+
elif t_status == "completed":
|
|
155
|
+
icon = "%s✓%s" % (BLUE, RST)
|
|
156
|
+
elif t_status == "failed":
|
|
157
|
+
icon = "%s✗%s" % (RED, RST)
|
|
158
|
+
else:
|
|
159
|
+
icon = "%s•%s" % (DIM, RST)
|
|
160
|
+
|
|
161
|
+
row = "%s %s%s%s" % (icon, BOLD, t_name, RST)
|
|
162
|
+
if t_type:
|
|
163
|
+
row += " %s(%s)%s" % (DIM, t_type, RST)
|
|
164
|
+
if os.environ.get("CBP_SUBAGENT_STATUSLINE_HIDE_DESCRIPTION") != "1" and t_desc:
|
|
165
|
+
row += " %s—%s %s" % (DIM, RST, t_desc)
|
|
166
|
+
if os.environ.get("CBP_SUBAGENT_STATUSLINE_HIDE_TOKENS") != "1" and t_tokens > 0:
|
|
167
|
+
row += " %s·%s %stokens:%s%s" % (DIM, RST, DIM, RST, fmt_k(t_tokens))
|
|
168
|
+
if t_start > 0:
|
|
169
|
+
row += " %s·%s %sage:%s%s" % (DIM, RST, DIM, RST, fmt_age_secs(t_start))
|
|
170
|
+
|
|
171
|
+
rendered = truncate(row)
|
|
172
|
+
out.append(
|
|
173
|
+
'{"id":%s,"content":%s}'
|
|
174
|
+
% (json.dumps(t_id, ensure_ascii=False), json.dumps(rendered, ensure_ascii=False))
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
sys.stdout.write(("\n".join(out) + "\n") if out else "")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
main()
|
|
182
|
+
except Exception:
|
|
183
|
+
sys.exit(0)
|
|
@@ -1,22 +1,65 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# Portability: bash 3.2+ (macOS default /bin/bash). UTF-8 multibyte chars in
|
|
3
3
|
# user-controlled fields are byte-iterated under 3.2 (over-counts visible width
|
|
4
|
-
# for non-ASCII content); acceptable safe-direction approximation
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
4
|
+
# for non-ASCII content); acceptable safe-direction approximation — and the node
|
|
5
|
+
# (.mjs) / python (.py) renderers iterate the SAME UTF-8 bytes so all three agree.
|
|
6
|
+
# @hook: NOT-A-HOOK (subagentStatusLine renderer, invoked via settings.json subagentStatusLine.command)
|
|
7
|
+
# Claude Code Subagent Status Line — multi-runtime dispatcher + bash renderer.
|
|
8
|
+
# One JSON line per active subagent. Output protocol per upstream docs:
|
|
8
9
|
# {"id": "<task_id>", "content": "<rendered_body>"}
|
|
9
10
|
# - Omitted tasks keep their default Claude Code rendering
|
|
10
11
|
# - Empty content strings hide the row
|
|
11
12
|
# NOT a PreToolUse/PostToolUse/Notification hook — do NOT register in hooks[].
|
|
12
13
|
#
|
|
13
|
-
#
|
|
14
|
+
# RENDERER SELECTION (per-device, gitignored .codebyplan/statusline.local.json):
|
|
15
|
+
# { "renderer": "bash" | "node" | "python" } — default bash, falls back to bash
|
|
16
|
+
# when the chosen runtime is unavailable. Set via `codebyplan statusline …`.
|
|
17
|
+
#
|
|
18
|
+
# CONFIG: honours `no_color` from the committed .codebyplan/statusline.json. The six
|
|
19
|
+
# main-statusline line toggles do NOT apply to subagent rows.
|
|
20
|
+
#
|
|
21
|
+
# ENV-VAR OVERRIDES (env > config > default)
|
|
14
22
|
# CBP_SUBAGENT_STATUSLINE_HIDE_DESCRIPTION=1 omit DESCRIPTION segment
|
|
15
23
|
# CBP_SUBAGENT_STATUSLINE_HIDE_TOKENS=1 omit tokens:N segment
|
|
16
24
|
# CBP_SUBAGENT_STATUSLINE_NO_COLOR=1 strip all ANSI codes (also honoured by $NO_COLOR)
|
|
25
|
+
#
|
|
26
|
+
# TEST SEAMS (no effect in normal use)
|
|
27
|
+
# CBP_STATUSLINE_ROOT=<dir> override the .codebyplan/ lookup root (default: repo root)
|
|
28
|
+
# CBP_STATUSLINE_NOW=<epoch> override "now" for age rendering (determinism)
|
|
29
|
+
|
|
30
|
+
# ============================================================
|
|
31
|
+
# DISPATCHER — resolve root + renderer, delegate when possible
|
|
32
|
+
# ============================================================
|
|
33
|
+
CBP_HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null)" || CBP_HOOK_DIR="."
|
|
34
|
+
CBP_ROOT="${CBP_STATUSLINE_ROOT:-}"
|
|
35
|
+
if [ -z "$CBP_ROOT" ]; then
|
|
36
|
+
CBP_ROOT="$(cd "$CBP_HOOK_DIR/../.." && pwd 2>/dev/null)" || CBP_ROOT="$PWD"
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
CBP_RENDERER="bash"
|
|
40
|
+
CBP_LOCAL_CFG="$CBP_ROOT/.codebyplan/statusline.local.json"
|
|
41
|
+
if [ -f "$CBP_LOCAL_CFG" ] && command -v jq >/dev/null 2>&1; then
|
|
42
|
+
_r="$(jq -r '.renderer // empty' "$CBP_LOCAL_CFG" 2>/dev/null)"
|
|
43
|
+
case "$_r" in bash|node|python) CBP_RENDERER="$_r" ;; esac
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
if [ "$CBP_RENDERER" = "node" ] && command -v node >/dev/null 2>&1 \
|
|
47
|
+
&& [ -f "$CBP_HOOK_DIR/cbp-subagent-statusline.mjs" ]; then
|
|
48
|
+
CBP_STATUSLINE_ROOT="$CBP_ROOT" exec node "$CBP_HOOK_DIR/cbp-subagent-statusline.mjs"
|
|
49
|
+
fi
|
|
50
|
+
if [ "$CBP_RENDERER" = "python" ] && command -v python3 >/dev/null 2>&1 \
|
|
51
|
+
&& [ -f "$CBP_HOOK_DIR/cbp-subagent-statusline.py" ]; then
|
|
52
|
+
CBP_STATUSLINE_ROOT="$CBP_ROOT" exec python3 "$CBP_HOOK_DIR/cbp-subagent-statusline.py"
|
|
53
|
+
fi
|
|
54
|
+
# Fall through → inline bash renderer (default + universal fallback).
|
|
17
55
|
|
|
18
56
|
INPUT=$(cat)
|
|
19
57
|
|
|
58
|
+
# Byte-deterministic string ops: force the C locale so the truncation walk below
|
|
59
|
+
# counts UTF-8 BYTES (locale-independent), matching the .mjs/.py byte iteration.
|
|
60
|
+
# Output bytes are unaffected (multibyte content passes through verbatim).
|
|
61
|
+
export LC_ALL=C
|
|
62
|
+
|
|
20
63
|
# One jq pass for base fields (cwd, session_id, columns)
|
|
21
64
|
eval "$(echo "$INPUT" | jq -r '
|
|
22
65
|
@sh "CWD=\(.cwd // "")",
|
|
@@ -24,9 +67,17 @@ eval "$(echo "$INPUT" | jq -r '
|
|
|
24
67
|
@sh "COLUMNS=\(.columns // 0)"
|
|
25
68
|
')"
|
|
26
69
|
|
|
27
|
-
# ----
|
|
70
|
+
# ---- Config: no_color from .codebyplan/statusline.json -----------------------
|
|
71
|
+
CFG_NO_COLOR=false
|
|
72
|
+
CBP_CFG="$CBP_ROOT/.codebyplan/statusline.json"
|
|
73
|
+
if [ -f "$CBP_CFG" ] && command -v jq >/dev/null 2>&1; then
|
|
74
|
+
# `== true` (NOT jq `//`): only an explicit true enables no_color.
|
|
75
|
+
eval "$(jq -r '"CFG_NO_COLOR=\(.no_color == true)"' "$CBP_CFG" 2>/dev/null)"
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# ---- Colour setup (env > config) ---------------------------------------------
|
|
28
79
|
# Only the colours referenced by the render block below are defined here.
|
|
29
|
-
if [ -n "${NO_COLOR:-}" ] || [ "${CBP_SUBAGENT_STATUSLINE_NO_COLOR:-0}" = "1" ]; then
|
|
80
|
+
if [ -n "${NO_COLOR:-}" ] || [ "${CBP_SUBAGENT_STATUSLINE_NO_COLOR:-0}" = "1" ] || [ "$CFG_NO_COLOR" = "true" ]; then
|
|
30
81
|
RST=''; DIM=''; BOLD=''; GREEN=''; RED=''; BLUE=''
|
|
31
82
|
else
|
|
32
83
|
RST='\033[0m'
|
|
@@ -37,25 +88,29 @@ else
|
|
|
37
88
|
BLUE='\033[34m'
|
|
38
89
|
fi
|
|
39
90
|
|
|
40
|
-
# ---- Token/size formatter K / M
|
|
91
|
+
# ---- Token/size formatter K / M — integer round-half-up (cross-runtime) ------
|
|
41
92
|
fmt_k() {
|
|
42
93
|
local val=$1
|
|
43
94
|
if [ "$val" -ge 1000000 ] 2>/dev/null; then
|
|
44
|
-
|
|
95
|
+
local t=$(( (val + 50000) / 100000 ))
|
|
96
|
+
printf "%d.%dM" $(( t / 10 )) $(( t % 10 ))
|
|
45
97
|
elif [ "$val" -ge 1000 ] 2>/dev/null; then
|
|
46
|
-
|
|
98
|
+
local t=$(( (val + 50) / 100 ))
|
|
99
|
+
printf "%d.%dK" $(( t / 10 )) $(( t % 10 ))
|
|
47
100
|
else
|
|
48
101
|
printf "%d" "$val"
|
|
49
102
|
fi
|
|
50
103
|
}
|
|
51
104
|
|
|
52
|
-
# ----
|
|
53
|
-
|
|
54
|
-
|
|
105
|
+
# ---- "now" with test seam ----------------------------------------------------
|
|
106
|
+
cbp_now() {
|
|
107
|
+
if [ -n "${CBP_STATUSLINE_NOW:-}" ]; then printf '%s' "$CBP_STATUSLINE_NOW"; else date +%s; fi
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# ---- Age formatter: epoch seconds → "Xs" / "XmYs" / "XhYm" ------------------
|
|
55
111
|
fmt_age_secs() {
|
|
56
112
|
local start=$1
|
|
57
|
-
local now
|
|
58
|
-
now=$(date +%s)
|
|
113
|
+
local now; now=$(cbp_now)
|
|
59
114
|
local delta=$(( now - start ))
|
|
60
115
|
[ "$delta" -lt 0 ] && delta=0
|
|
61
116
|
if [ "$delta" -ge 3600 ]; then
|
|
@@ -67,18 +122,16 @@ fmt_age_secs() {
|
|
|
67
122
|
fi
|
|
68
123
|
}
|
|
69
124
|
|
|
70
|
-
# ---- Strip ANSI codes for visible-length measurement (column truncation) ----
|
|
71
|
-
strip_ansi() {
|
|
72
|
-
printf '%s' "$1" | sed $'s/\033\\[[0-9;]*m//g'
|
|
73
|
-
}
|
|
74
|
-
|
|
75
125
|
# ---- JSON-string-escape a bash variable (embed in {"id":..., "content":...}) -
|
|
76
126
|
json_escape() {
|
|
77
127
|
printf '%s' "$1" | jq -Rs '.'
|
|
78
128
|
}
|
|
79
129
|
|
|
80
|
-
# ---- Per-task render: one jq
|
|
81
|
-
#
|
|
130
|
+
# ---- Per-task render: one jq US-delimited emission, looped in bash ----------
|
|
131
|
+
# Join with the Unit Separator (U+001F, non-whitespace) rather than @tsv: a tab is
|
|
132
|
+
# IFS-whitespace, so `IFS=$'\t' read` collapses consecutive tabs and an EMPTY field
|
|
133
|
+
# (e.g. blank description) would shift every later field. US is non-whitespace, so
|
|
134
|
+
# empty fields are preserved — matching the .mjs/.py object iteration.
|
|
82
135
|
echo "$INPUT" | jq -r '
|
|
83
136
|
.tasks // [] | .[] | [
|
|
84
137
|
.id // "",
|
|
@@ -88,8 +141,8 @@ echo "$INPUT" | jq -r '
|
|
|
88
141
|
.description // "",
|
|
89
142
|
(.startTime // 0 | tostring),
|
|
90
143
|
(.tokenCount // 0 | tostring)
|
|
91
|
-
] |
|
|
92
|
-
' | while IFS=$'\
|
|
144
|
+
] | join("\u001f")
|
|
145
|
+
' | while IFS=$'\037' read -r T_ID T_NAME T_TYPE T_STATUS T_DESC T_START T_TOKENS; do
|
|
93
146
|
# Skip blank rows (empty tasks[] → no output)
|
|
94
147
|
[ -z "$T_ID" ] && continue
|
|
95
148
|
|
|
@@ -123,14 +176,12 @@ echo "$INPUT" | jq -r '
|
|
|
123
176
|
# Apply printf to interpret escape sequences, then truncate to $COLUMNS
|
|
124
177
|
RENDERED=$(printf '%b' "$ROW")
|
|
125
178
|
if [ "$COLUMNS" -gt 0 ] 2>/dev/null; then
|
|
126
|
-
# ANSI-aware iterative truncation: walk RENDERED
|
|
127
|
-
# \033[...letter escape sequences as zero-width and ordinary
|
|
179
|
+
# ANSI-aware iterative truncation: walk RENDERED byte-by-byte treating
|
|
180
|
+
# \033[...letter escape sequences as zero-width and ordinary bytes as
|
|
128
181
|
# one-width. Stop when the visible counter reaches COLUMNS - 3, then
|
|
129
|
-
# append RST + ellipsis.
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
# multi-byte UTF-8 in user-controlled descriptions counts as multiple
|
|
133
|
-
# visible units — acceptable approximation for the ASCII-dominant case.
|
|
182
|
+
# append RST + ellipsis. Byte-based slicing means non-ASCII multi-byte
|
|
183
|
+
# UTF-8 counts as multiple visible units — the .mjs/.py renderers iterate
|
|
184
|
+
# the same UTF-8 bytes, so all three stay byte-identical.
|
|
134
185
|
TARGET=$(( COLUMNS - 3 ))
|
|
135
186
|
[ "$TARGET" -lt 0 ] && TARGET=0
|
|
136
187
|
RAW_LEN=${#RENDERED}
|
|
@@ -150,12 +201,11 @@ echo "$INPUT" | jq -r '
|
|
|
150
201
|
fi
|
|
151
202
|
i=$(( i + 1 ))
|
|
152
203
|
done
|
|
153
|
-
# Post-loop drain: every rendered row ends with a trailing ${RST}
|
|
154
|
-
#
|
|
155
|
-
#
|
|
156
|
-
#
|
|
157
|
-
#
|
|
158
|
-
# or end-of-string.
|
|
204
|
+
# Post-loop drain: every rendered row ends with a trailing ${RST}. Without
|
|
205
|
+
# this drain a row whose visible content fits within COLUMNS still has
|
|
206
|
+
# `i < RAW_LEN` (pointing inside the trailing reset), spuriously triggering
|
|
207
|
+
# truncation. Walk past any in-progress escape and any further escapes until
|
|
208
|
+
# a real visible byte or end-of-string.
|
|
159
209
|
while [ $i -lt $RAW_LEN ]; do
|
|
160
210
|
ch="${RENDERED:$i:1}"
|
|
161
211
|
if [ "$in_esc" = "1" ]; then
|
|
@@ -171,8 +221,6 @@ echo "$INPUT" | jq -r '
|
|
|
171
221
|
fi
|
|
172
222
|
done
|
|
173
223
|
if [ $i -lt $RAW_LEN ]; then
|
|
174
|
-
# printf '%b' converts the literal-text RST (`\033[0m`) into an actual
|
|
175
|
-
# ESC byte sequence, matching the byte semantics of RENDERED's prefix.
|
|
176
224
|
RENDERED="${RENDERED:0:$i}$(printf '%b' "$RST")..."
|
|
177
225
|
fi
|
|
178
226
|
fi
|
|
@@ -76,7 +76,7 @@ Stop here.
|
|
|
76
76
|
|
|
77
77
|
Collect QA results from all tasks and rounds. Build checkpoint-level QA summary:
|
|
78
78
|
- Count auto checks passed/failed
|
|
79
|
-
- List
|
|
79
|
+
- List default-checklist items still pending review
|
|
80
80
|
|
|
81
81
|
If any critical failures, warn the user but don't block.
|
|
82
82
|
|
|
@@ -184,7 +184,7 @@ For each screenshot in `e2e_screenshots[]`:
|
|
|
184
184
|
|
|
185
185
|
Populate `screenshot_review` totals.
|
|
186
186
|
|
|
187
|
-
**Do not attempt to auto-fix `rendered_visual` or `baseline_regression` findings** — they
|
|
187
|
+
**Do not attempt to auto-fix `rendered_visual` or `baseline_regression` findings** — they surface as a blocking gate at `/cbp-round-end` Step 7 (accept-or-fix) and feed the fix loop, because the root cause is typically in app code/data, not in the SCSS.
|
|
188
188
|
|
|
189
189
|
### Phase 7: Aggregate Findings
|
|
190
190
|
|
|
@@ -258,5 +258,5 @@ Go beyond fixing violations — actively improve visual quality. If spacing coul
|
|
|
258
258
|
- **Also invoked by**: `/cbp-checkpoint-check` (TASK-2 deliverable, future) with screenshots from a whole-checkpoint e2e run
|
|
259
259
|
- **Consumes**: `e2e_screenshots[]` from `round.context.e2e_output.screenshots` (populated by `test-e2e-agent` at `/cbp-round-execute` Step 5)
|
|
260
260
|
- **Output written to**: `round.context.frontend_ui_review` — when invoked twice per round, the second invocation merges with the first
|
|
261
|
-
- **
|
|
261
|
+
- **Downstream gate**: this skill emits `findings[]` only. Baseline-regression findings surface as a BLOCKING gate at `/cbp-round-end` Step 7 (baselines never auto-accepted); rendered-visual critical findings are surfaced in the Step 7 findings presentation.
|
|
262
262
|
- **Paired with**: `frontend-design` (pre-implementation aesthetic decision), `frontend-ux` (interaction-quality self-review, also Step 3.8)
|
|
@@ -55,46 +55,26 @@ Build the files list with approval status:
|
|
|
55
55
|
**claude_approved**: `true` if cbp-testing-qa-agent passed for this file. `false` if issues remain.
|
|
56
56
|
**user_approved**: Always `false` initially. User approves via git staging or web UI.
|
|
57
57
|
|
|
58
|
-
### Step 3: Collect
|
|
58
|
+
### Step 3: Collect QA Results
|
|
59
59
|
|
|
60
60
|
**No QA runs here** — all QA was already executed by per-wave `cbp-testing-qa-agent` inside `/cbp-round-execute` Step 5.
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
#### 3a — Collect items from agent outputs
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
Collect from round context (all three sources are agent-derived):
|
|
64
|
+
Collect from round context:
|
|
67
65
|
|
|
68
66
|
- **Auto items**: from `testing_qa_output.auto_qa.items`
|
|
69
|
-
- **User items (single-source)**: from `testing_qa_output.user_qa.items` (Phase 4b.1 design-source comparison + Phase 4b.2 mechanical-sweep spot-check + Phase 4b.0 connection smoke — all derivable from cbp-testing-qa-agent's own data)
|
|
70
67
|
- **Default items**: from `testing_qa_output.default_checklist.items`
|
|
71
68
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
Read `round.context.frontend_ui_review.findings[]` (populated by the `cbp-frontend-ui` skill at `/cbp-round-execute` Step 5b under `phase: 'screenshot_review'`). For each finding, emit a `user_qa` item per the rules below.
|
|
75
|
-
|
|
76
|
-
| Finding `category` | Finding `severity` | Emitted user_qa item |
|
|
77
|
-
|--------------------|-------------------|---------------------|
|
|
78
|
-
| `baseline_regression` | any | `{ type: 'user', check: 'Visual baseline regression — {page_or_screen}', status: 'pending', round_number: N, instructions: 'Open the diff PNG at `{screenshot}` (the `-diff.png` sibling shows pixel differences). Pixel-diff was `{baseline_diff_pct}%`. Decide: (a) regression — add a task to fix, OR (b) new rendering is correct — run `pnpm exec playwright test --update-snapshots` in `apps/{app}` and commit the updated baselines. Do NOT proceed until a decision is recorded.' }` |
|
|
79
|
-
| `rendered_visual` | `critical` | `{ type: 'user', check: 'Rendered-visual critical — {page_or_screen}', status: 'pending', round_number: N, instructions: 'Open the screenshot at `{screenshot}`. The cbp-frontend-ui review flagged a critical rendering issue: `{finding.issue}`. Suggested fix: `{finding.suggestion}`. Decide whether this needs a fix-round before proceeding.' }` |
|
|
80
|
-
| `rendered_visual` | `warning` | (no user_qa item; finding stays in `frontend_ui_review.findings` and surfaces via Step 7 findings presentation if relevant) |
|
|
81
|
-
| Other categories | any | (no user_qa item from this step) |
|
|
82
|
-
|
|
83
|
-
Skip Step 3b entirely when `round.context.frontend_ui_review` is absent (no e2e ran, or screenshot-review phase didn't execute).
|
|
84
|
-
|
|
85
|
-
This is the required user gate for baseline updates — baselines are NEVER auto-accepted.
|
|
86
|
-
|
|
87
|
-
#### 3c — Merge
|
|
88
|
-
|
|
89
|
-
Combine the single-source items (3a) and cross-source items (3b) into a single `user_qa[]` for the round. Merge with previous rounds (supersede items for re-modified files, preserve verified items).
|
|
69
|
+
Merge with previous rounds (supersede items for re-modified files, preserve verified items).
|
|
90
70
|
|
|
91
71
|
### Step 4: Update Task Files and QA
|
|
92
72
|
|
|
93
73
|
Update via MCP:
|
|
94
74
|
|
|
95
75
|
- `update_task(task_id, files_changed: [...])` — merge with existing
|
|
96
|
-
- `update_round(round_id, files_changed: [...], qa: {items: [
|
|
97
|
-
- `update_task(task_id, qa: {items: [
|
|
76
|
+
- `update_round(round_id, files_changed: [...], qa: {items: [auto_qa items + default_checklist items]})` — round-specific
|
|
77
|
+
- `update_task(task_id, qa: {items: [auto_qa items + default_checklist items]})` — aggregated
|
|
98
78
|
|
|
99
79
|
### Step 5: Present Summary
|
|
100
80
|
|
|
@@ -134,6 +114,16 @@ Wait for agent to complete. If the spawn fails for any reason, apply the inline-
|
|
|
134
114
|
|
|
135
115
|
### Step 7: Present Findings
|
|
136
116
|
|
|
117
|
+
**Baseline-regression blocking gate**: before presenting code-review findings, check `round.context.frontend_ui_review.findings[]` for any entry with `category: 'baseline_regression'` (any severity). When one or more such findings exist, surface them as a BLOCKING decision that MUST be resolved before routing to `/cbp-round-update`:
|
|
118
|
+
|
|
119
|
+
- Present each regression: screenshot path, `baseline_diff_pct`, affected page/screen.
|
|
120
|
+
- Ask the user via AskUserQuestion to choose:
|
|
121
|
+
- **(a) Treat as regression** — add a fix-round to address the visual change, OR
|
|
122
|
+
- **(b) Accept the new baseline** — run `pnpm exec playwright test --update-snapshots` in `apps/{app}` and commit the updated baselines.
|
|
123
|
+
- Do NOT route to `/cbp-round-update` until the decision is recorded. Baselines are NEVER auto-accepted.
|
|
124
|
+
|
|
125
|
+
`rendered_visual` critical findings from `round.context.frontend_ui_review.findings[]` are surfaced in the normal findings presentation below (not as a separate gate).
|
|
126
|
+
|
|
137
127
|
**If `status: 'no_findings'`:** show `### Code Review\nNo issues found. Code looks good.` and skip to Step 8.
|
|
138
128
|
|
|
139
129
|
**If findings exist**, present them grouped by severity (table + per-finding details), then ask the user via AskUserQuestion which to fix: `all`, `1,2` (specific numbers), `none`, or `inline` (only when all findings qualify under the Trivial-Resolution Exception).
|
|
@@ -149,13 +149,13 @@ On pass, synthesise `testing_qa_output` inline per the procedure in `reference/i
|
|
|
149
149
|
|
|
150
150
|
Input contracts: `cbp-testing-qa-agent` receives `executor_output`, `testing_profile`, `has_ui_work` (see `agents/cbp-testing-qa-agent.md` Input Contract). `cbp-test-e2e-agent` receives `repo_id`, `round_number`, `files_changed`, `prior_round_files_changed` (full task aggregate when round_number ≥ 2), `whole_checkpoint_mode: false`, `test_strategy`, `pages_affected`, `has_auth`, `dev_server_port` (see `agents/cbp-test-e2e-agent.md` Input Contract for the full shape).
|
|
151
151
|
|
|
152
|
-
**Independence**: neither agent reads the other's output.
|
|
152
|
+
**Independence**: neither agent reads the other's output. Baseline-regression findings surface as a BLOCKING gate at `/cbp-round-end` Step 7 (an explicit accept-or-fix user decision; baselines are NEVER auto-accepted). Per-wave spawns MAY run in parallel with the next wave's executor when dependency order allows.
|
|
153
153
|
|
|
154
154
|
### Step 5b: Post-E2E Screenshot Review (cbp-frontend-ui Phase 6.5)
|
|
155
155
|
|
|
156
156
|
When `round.context.e2e_output.screenshots[]` is non-empty, invoke the `cbp-frontend-ui` skill with `phase: 'screenshot_review'` (input: `files_changed`, `e2e_screenshots: round.context.e2e_output.screenshots`, `context: { checkpoint_goal, round_requirements }`). Under this phase the skill runs only Phase 6.5 (Rendered-Output Visual Review) + 7 + 8 — Phases 1-6 (style) already ran inline at executor Step 3.8 with `phase: 'style_only'`.
|
|
157
157
|
|
|
158
|
-
Persist findings to `round.context.frontend_ui_review` (merge with Step 3.8's style-only output if present). `/cbp-round-end` Step
|
|
158
|
+
Persist findings to `round.context.frontend_ui_review` (merge with Step 3.8's style-only output if present). Baseline-regression findings surface as a BLOCKING gate at `/cbp-round-end` Step 7 (an explicit accept-or-fix user decision; baselines are NEVER auto-accepted); rendered_visual critical findings are surfaced in the Step 7 findings presentation. Neither auto-fails the round. cbp-testing-qa-agent does NOT read these findings (full independence per Step 5).
|
|
159
159
|
|
|
160
160
|
**Skip** when `round.context.e2e_output` is absent, `screenshots` is empty, or `testing_profile === 'claude_only'`.
|
|
161
161
|
|