@drunkcoding/agents-and-skills 0.0.18 → 0.0.19
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/.claude-plugin/marketplace.json +5 -5
- package/README.md +23 -0
- package/package.json +1 -1
- package/plugins/auto-power/.claude-plugin/plugin.json +1 -1
- package/plugins/html-effectiveness/.claude-plugin/plugin.json +1 -1
- package/plugins/plugin-validator/.claude-plugin/plugin.json +1 -1
- package/plugins/team-superpower/.claude-plugin/plugin.json +1 -1
- package/plugins/team-superpower/agents/backend-developer.md +93 -3
- package/plugins/team-superpower/agents/designer.md +17 -2
- package/plugins/team-superpower/agents/frontend-developer.md +94 -3
- package/plugins/team-superpower/agents/planner.md +62 -6
- package/plugins/team-superpower/agents/qa-engineer.md +73 -17
- package/plugins/team-superpower/agents/reviewer.md +71 -6
- package/plugins/team-superpower/agents/security-engineer.md +16 -1
- package/plugins/team-superpower/agents/software-architect.md +16 -1
- package/plugins/team-superpower/assets/AGENTS.md.template +23 -0
- package/plugins/team-superpower/assets/CLAUDE.md.template +17 -0
- package/plugins/team-superpower/assets/SESSION_README.md +47 -0
- package/plugins/team-superpower/commands/team-feature.md +217 -11
- package/plugins/team-superpower/hooks/task-completed.sh +203 -0
- package/plugins/team-superpower/hooks/task-created.sh +42 -1
- package/plugins/team-superpower/scripts/assess-complexity.sh +194 -0
- package/plugins/team-superpower/scripts/detect-stack.sh +39 -0
- package/plugins/team-superpower/scripts/wave-collision-check.sh +60 -0
- package/plugins/tech-graph/.claude-plugin/plugin.json +1 -1
- package/plugins/html-effectiveness/tests/_tmp.test.js +0 -58
- package/plugins/html-effectiveness/tests/fixtures/01-exploration-code-approaches.data.json +0 -3
- package/plugins/html-effectiveness/tests/fixtures/02-exploration-visual-designs.data.json +0 -3
- package/plugins/html-effectiveness/tests/fixtures/03-code-review-pr.data.json +0 -3
- package/plugins/html-effectiveness/tests/fixtures/04-code-understanding.data.json +0 -3
- package/plugins/html-effectiveness/tests/fixtures/05-design-system.data.json +0 -3
- package/plugins/html-effectiveness/tests/fixtures/06-component-variants.data.json +0 -3
- package/plugins/html-effectiveness/tests/fixtures/07-prototype-animation.data.json +0 -3
- package/plugins/html-effectiveness/tests/fixtures/08-prototype-interaction.data.json +0 -3
- package/plugins/html-effectiveness/tests/fixtures/09-slide-deck.data.json +0 -3
- package/plugins/html-effectiveness/tests/fixtures/10-svg-illustrations.data.json +0 -3
- package/plugins/html-effectiveness/tests/fixtures/11-status-report.data.json +0 -37
- package/plugins/html-effectiveness/tests/fixtures/12-incident-report.data.json +0 -3
- package/plugins/html-effectiveness/tests/fixtures/13-flowchart-diagram.data.json +0 -3
- package/plugins/html-effectiveness/tests/fixtures/14-research-feature-explainer.data.json +0 -3
- package/plugins/html-effectiveness/tests/fixtures/15-research-concept-explainer.data.json +0 -3
- package/plugins/html-effectiveness/tests/fixtures/16-implementation-plan.data.json +0 -3
- package/plugins/html-effectiveness/tests/fixtures/17-pr-writeup.data.json +0 -3
- package/plugins/html-effectiveness/tests/fixtures/18-editor-triage-board.data.json +0 -3
- package/plugins/html-effectiveness/tests/fixtures/19-editor-feature-flags.data.json +0 -3
- package/plugins/html-effectiveness/tests/fixtures/20-editor-prompt-tuner.data.json +0 -3
- package/plugins/html-effectiveness/tests/fixtures/_canned.data.json +0 -7
- package/plugins/html-effectiveness/tests/fixtures/_canned.html.tmpl +0 -7
- package/plugins/html-effectiveness/tests/fixtures/_canned.manifest.json +0 -18
- package/plugins/html-effectiveness/tests/manifest.test.js +0 -61
- package/plugins/html-effectiveness/tests/mustache.test.js +0 -47
- package/plugins/html-effectiveness/tests/render.test.js +0 -118
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# assess-complexity.sh — encode the team-superpower v3 heuristic ladder.
|
|
3
|
+
#
|
|
4
|
+
# Inputs:
|
|
5
|
+
# $1 — owner's launch message (required)
|
|
6
|
+
# $2 — repo root (optional, default $PWD); reads CLAUDE.md's
|
|
7
|
+
# security.domain to bias size and stack to seed `shape:`.
|
|
8
|
+
#
|
|
9
|
+
# Output: YAML on stdout. Always emits `mode:`, `shape:`, `mode_reasoning:`.
|
|
10
|
+
# Emits `size:` only when mode == team.
|
|
11
|
+
#
|
|
12
|
+
# Exit codes:
|
|
13
|
+
# 0 — confident decision
|
|
14
|
+
# 1 — ambiguous (mode could plausibly be single-agent OR team; defaults to
|
|
15
|
+
# team and notes the ambiguity in mode_reasoning).
|
|
16
|
+
#
|
|
17
|
+
# This script never spawns or writes outside stdout. The lead handles writes.
|
|
18
|
+
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
if [ "${1:-}" = "" ]; then
|
|
22
|
+
echo "usage: assess-complexity.sh <launch-message> [repo-root]" >&2
|
|
23
|
+
exit 2
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
MSG="$1"
|
|
27
|
+
ROOT="${2:-$PWD}"
|
|
28
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
29
|
+
DETECT="$SCRIPT_DIR/detect-stack.sh"
|
|
30
|
+
PARSE="$SCRIPT_DIR/parse-claudemd.sh"
|
|
31
|
+
|
|
32
|
+
lc="$(printf '%s' "$MSG" | tr '[:upper:]' '[:lower:]')"
|
|
33
|
+
length="${#MSG}"
|
|
34
|
+
|
|
35
|
+
# --- Discovery / size keyword tables --------------------------------------
|
|
36
|
+
|
|
37
|
+
discovery_keywords=(
|
|
38
|
+
"design" "architecture" "system" "flow" "feature" "epic" "refactor"
|
|
39
|
+
"migrate" "migrate to" "replace with" "replace" "rewrite" "redesign"
|
|
40
|
+
"overhaul"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
trivial_keywords=(
|
|
44
|
+
"fix typo" "typo in" "rename" "rename variable" "update copy"
|
|
45
|
+
"update text" "change wording" "bump version" "update readme"
|
|
46
|
+
"comment out" "add comment" "remove unused" "format" "prettify"
|
|
47
|
+
"lint fix"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
tiny_scope_phrases=(
|
|
51
|
+
"one line" "single line" "one file" "just change" "quick fix"
|
|
52
|
+
"tiny" "trivial"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
small_scope_verbs=(
|
|
56
|
+
"add" "create" "new " "introduce" "fix"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
small_scope_nouns=(
|
|
60
|
+
"endpoint" "/" "component" "field" "column" "validation"
|
|
61
|
+
"bug" "error" "test" "migration" "button" "page" "route"
|
|
62
|
+
"controller" "service"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
regulated_keywords=(
|
|
66
|
+
"compliance" "audit" "regulatory" "pii" "pci" "gdpr" "hipaa"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
low_stakes_keywords=(
|
|
70
|
+
"prototype" "spike" "internal-only" "experiment" "poc"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
team_request_keywords=(
|
|
74
|
+
"team" "agents" "full feature"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
contains_any() {
|
|
78
|
+
# contains_any <text-lc> <array-name>
|
|
79
|
+
# Compatible with Bash 3.2 (no namerefs).
|
|
80
|
+
local txt="$1"
|
|
81
|
+
local arr_name="$2"
|
|
82
|
+
eval 'local kw; for kw in "${'"$arr_name"'[@]}"; do
|
|
83
|
+
case "$txt" in *"$kw"*) return 0 ;; esac
|
|
84
|
+
done'
|
|
85
|
+
return 1
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# Rung 1: trivial / tiny-scope / single named file
|
|
89
|
+
single_file_match=0
|
|
90
|
+
# Detect a single path literal in the message (very rough: looks like x/y or x.y).
|
|
91
|
+
if printf '%s' "$MSG" | grep -qE '(^|[[:space:]])[A-Za-z0-9_./-]+\.[A-Za-z0-9]+([[:space:]]|$)'; then
|
|
92
|
+
# Count distinct file-ish tokens; if exactly 1, treat as named file.
|
|
93
|
+
count="$(printf '%s\n' "$MSG" | grep -oE '(^|[[:space:]])[A-Za-z0-9_./-]+\.[A-Za-z0-9]+([[:space:]]|$)' | tr -d '[:space:]' | sort -u | wc -l | tr -d ' ')"
|
|
94
|
+
if [ "$count" = "1" ]; then single_file_match=1; fi
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
solo=0
|
|
98
|
+
solo_why=""
|
|
99
|
+
if contains_any "$lc" trivial_keywords; then solo=1; solo_why="trivial keyword match";
|
|
100
|
+
elif contains_any "$lc" tiny_scope_phrases; then solo=1; solo_why="tiny-scope phrase match";
|
|
101
|
+
elif [ "$single_file_match" = "1" ]; then solo=1; solo_why="single named file in message";
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
# Rung 2: small-scope + single-side + no discovery language
|
|
105
|
+
side="none"
|
|
106
|
+
if [ -x "$DETECT" ] || [ -f "$DETECT" ]; then
|
|
107
|
+
side="$(bash "$DETECT" detect-side "$MSG" 2>/dev/null || echo none)"
|
|
108
|
+
fi
|
|
109
|
+
single_agent=0
|
|
110
|
+
single_agent_why=""
|
|
111
|
+
if [ "$solo" = "0" ]; then
|
|
112
|
+
if contains_any "$lc" small_scope_verbs \
|
|
113
|
+
&& contains_any "$lc" small_scope_nouns \
|
|
114
|
+
&& { [ "$side" = "be-only" ] || [ "$side" = "fe-only" ]; } \
|
|
115
|
+
&& ! contains_any "$lc" discovery_keywords; then
|
|
116
|
+
single_agent=1
|
|
117
|
+
single_agent_why="small-scope verb+noun + single-side ($side) + no discovery language"
|
|
118
|
+
fi
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# Rung 3: team (default)
|
|
122
|
+
verb_count=0
|
|
123
|
+
# Cheap multi-verb heuristic: count occurrences of " and " between action words.
|
|
124
|
+
verb_count="$(printf '%s' "$lc" | { grep -oE '[[:space:]]and[[:space:]]' || true; } | wc -l | tr -d ' ')"
|
|
125
|
+
long_message=0
|
|
126
|
+
if [ "$length" -gt 200 ]; then long_message=1; fi
|
|
127
|
+
|
|
128
|
+
team=0
|
|
129
|
+
team_why=""
|
|
130
|
+
if [ "$solo" = "0" ] && [ "$single_agent" = "0" ]; then
|
|
131
|
+
team=1
|
|
132
|
+
if [ "$side" = "mixed" ]; then team_why="multi-side signal (BE+FE)";
|
|
133
|
+
elif contains_any "$lc" discovery_keywords; then team_why="discovery language match";
|
|
134
|
+
elif [ "$verb_count" -ge 1 ]; then team_why="multi-verb message ($verb_count 'and' joiners)";
|
|
135
|
+
elif [ "$long_message" = "1" ]; then team_why="long message (>200 chars)";
|
|
136
|
+
elif contains_any "$lc" team_request_keywords; then team_why="explicit team request";
|
|
137
|
+
else team_why="rung-1 and rung-2 did not match; defaulting to team";
|
|
138
|
+
fi
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
# Resolve mode
|
|
142
|
+
if [ "$solo" = "1" ]; then mode="solo"; reason="Rung 1 matched: $solo_why."
|
|
143
|
+
elif [ "$single_agent" = "1" ]; then mode="single-agent"; reason="Rung 2 matched: $single_agent_why."
|
|
144
|
+
else mode="team"; reason="Rung 3 matched: $team_why."
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
# Size (team only)
|
|
148
|
+
size=""
|
|
149
|
+
size_why=""
|
|
150
|
+
if [ "$mode" = "team" ]; then
|
|
151
|
+
domain=""
|
|
152
|
+
if [ -f "$ROOT/CLAUDE.md" ] && [ -f "$PARSE" ]; then
|
|
153
|
+
domain="$(bash "$PARSE" get security.domain "$ROOT/CLAUDE.md" 2>/dev/null || true)"
|
|
154
|
+
fi
|
|
155
|
+
if [ "$domain" = "payments" ] || [ "$domain" = "healthcare" ]; then
|
|
156
|
+
size="full"; size_why="security.domain=$domain"
|
|
157
|
+
elif contains_any "$lc" regulated_keywords; then
|
|
158
|
+
size="full"; size_why="regulated keyword match"
|
|
159
|
+
elif contains_any "$lc" low_stakes_keywords; then
|
|
160
|
+
size="minimal"; size_why="low-stakes keyword match"
|
|
161
|
+
else
|
|
162
|
+
size="standard"; size_why="default"
|
|
163
|
+
fi
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
# Shape: prefer CLAUDE.md, fall back to mixed.
|
|
167
|
+
shape=""
|
|
168
|
+
if [ -f "$ROOT/CLAUDE.md" ] && [ -f "$PARSE" ]; then
|
|
169
|
+
shape="$(bash "$PARSE" shape "$ROOT/CLAUDE.md" 2>/dev/null || true)"
|
|
170
|
+
fi
|
|
171
|
+
if [ -z "$shape" ] || [ "$shape" = "none" ]; then
|
|
172
|
+
# Fall back to side detection: mixed → full-stack; be-only / fe-only as-is.
|
|
173
|
+
case "$side" in
|
|
174
|
+
mixed) shape="full-stack" ;;
|
|
175
|
+
be-only) shape="be-only" ;;
|
|
176
|
+
fe-only) shape="fe-only" ;;
|
|
177
|
+
*) shape="full-stack" ;;
|
|
178
|
+
esac
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
# Emit
|
|
182
|
+
printf 'mode: %s\n' "$mode"
|
|
183
|
+
if [ "$mode" = "team" ]; then
|
|
184
|
+
printf 'size: %s\n' "$size"
|
|
185
|
+
fi
|
|
186
|
+
printf 'shape: %s\n' "$shape"
|
|
187
|
+
printf 'mode_reasoning: |\n'
|
|
188
|
+
printf ' %s\n' "$reason"
|
|
189
|
+
if [ "$mode" = "team" ]; then
|
|
190
|
+
printf ' Size: %s — %s.\n' "$size" "$size_why"
|
|
191
|
+
fi
|
|
192
|
+
printf ' side_signal: %s; verb_joiners: %d; length: %d chars.\n' "$side" "$verb_count" "$length"
|
|
193
|
+
|
|
194
|
+
exit 0
|
|
@@ -22,6 +22,45 @@
|
|
|
22
22
|
|
|
23
23
|
set -euo pipefail
|
|
24
24
|
|
|
25
|
+
# ---- subcommand: detect-side ---------------------------------------------
|
|
26
|
+
# Usage:
|
|
27
|
+
# bash detect-stack.sh detect-side "<launch message>"
|
|
28
|
+
# Outputs one of: be-only | fe-only | mixed | none
|
|
29
|
+
# Used by scripts/assess-complexity.sh for the rung-2 single-side signal.
|
|
30
|
+
if [ "${1:-}" = "detect-side" ]; then
|
|
31
|
+
shift
|
|
32
|
+
text="${1:-}"
|
|
33
|
+
# Normalize to lowercase for case-insensitive matching.
|
|
34
|
+
lc="$(printf '%s' "$text" | tr '[:upper:]' '[:lower:]')"
|
|
35
|
+
|
|
36
|
+
be_keywords=(
|
|
37
|
+
"endpoint" "api" "/api" "/auth" "/health" "route" "controller" "service"
|
|
38
|
+
"repository" "schema" "migration" "database" "column" "table"
|
|
39
|
+
"queue" "worker" "cron" "webhook" "grpc" "rpc"
|
|
40
|
+
)
|
|
41
|
+
fe_keywords=(
|
|
42
|
+
"component" "page" "form" "button" "modal" "input" "field"
|
|
43
|
+
"ui" "layout" "css" "tailwind" "tsx" "jsx" "react" "vue"
|
|
44
|
+
"svelte" "stylesheet" "wireframe" "design system" "screen"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
has_be=0
|
|
48
|
+
has_fe=0
|
|
49
|
+
for kw in "${be_keywords[@]}"; do
|
|
50
|
+
case "$lc" in *"$kw"*) has_be=1; break ;; esac
|
|
51
|
+
done
|
|
52
|
+
for kw in "${fe_keywords[@]}"; do
|
|
53
|
+
case "$lc" in *"$kw"*) has_fe=1; break ;; esac
|
|
54
|
+
done
|
|
55
|
+
|
|
56
|
+
if [ "$has_be" -eq 1 ] && [ "$has_fe" -eq 1 ]; then echo "mixed"
|
|
57
|
+
elif [ "$has_be" -eq 1 ]; then echo "be-only"
|
|
58
|
+
elif [ "$has_fe" -eq 1 ]; then echo "fe-only"
|
|
59
|
+
else echo "none"
|
|
60
|
+
fi
|
|
61
|
+
exit 0
|
|
62
|
+
fi
|
|
63
|
+
|
|
25
64
|
ROOT="${1:-$PWD}"
|
|
26
65
|
cd "$ROOT"
|
|
27
66
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# wave-collision-check.sh — detect file-scope collisions inside a wave.
|
|
3
|
+
#
|
|
4
|
+
# Input (stdin): one task per line, space-separated:
|
|
5
|
+
# <task-id> <file1> [<file2> ...]
|
|
6
|
+
# Files are normalized (lowercase, leading ./ stripped, slashes preserved).
|
|
7
|
+
#
|
|
8
|
+
# Exit codes:
|
|
9
|
+
# 0 — no collision
|
|
10
|
+
# 1 — at least one pair of tasks share a normalized file
|
|
11
|
+
#
|
|
12
|
+
# Stdout on collision: one line per colliding pair:
|
|
13
|
+
# COLLISION <task-i> <task-j> <file>
|
|
14
|
+
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
normalize() {
|
|
18
|
+
# lowercase + strip leading ./
|
|
19
|
+
printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's|^\./||'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
tasks=()
|
|
23
|
+
files_list=()
|
|
24
|
+
|
|
25
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
26
|
+
[ -z "$line" ] && continue
|
|
27
|
+
# shellcheck disable=SC2206
|
|
28
|
+
parts=($line)
|
|
29
|
+
task="${parts[0]}"
|
|
30
|
+
files=()
|
|
31
|
+
for ((i=1; i<${#parts[@]}; i++)); do
|
|
32
|
+
files+=("$(normalize "${parts[$i]}")")
|
|
33
|
+
done
|
|
34
|
+
tasks+=("$task")
|
|
35
|
+
files_list+=("${files[*]}")
|
|
36
|
+
done
|
|
37
|
+
|
|
38
|
+
collisions=0
|
|
39
|
+
n=${#tasks[@]}
|
|
40
|
+
for ((i=0; i<n; i++)); do
|
|
41
|
+
for ((j=i+1; j<n; j++)); do
|
|
42
|
+
# shellcheck disable=SC2206
|
|
43
|
+
fi=(${files_list[$i]})
|
|
44
|
+
# shellcheck disable=SC2206
|
|
45
|
+
fj=(${files_list[$j]})
|
|
46
|
+
for f1 in "${fi[@]}"; do
|
|
47
|
+
[ -z "$f1" ] && continue
|
|
48
|
+
for f2 in "${fj[@]}"; do
|
|
49
|
+
[ -z "$f2" ] && continue
|
|
50
|
+
if [ "$f1" = "$f2" ]; then
|
|
51
|
+
printf 'COLLISION %s %s %s\n' "${tasks[$i]}" "${tasks[$j]}" "$f1"
|
|
52
|
+
collisions=$((collisions+1))
|
|
53
|
+
fi
|
|
54
|
+
done
|
|
55
|
+
done
|
|
56
|
+
done
|
|
57
|
+
done
|
|
58
|
+
|
|
59
|
+
if [ "$collisions" -gt 0 ]; then exit 1; fi
|
|
60
|
+
exit 0
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { existsSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { getTmpDir, cleanup, withTmp, ROOT } from '../scripts/_tmp.js';
|
|
6
|
-
|
|
7
|
-
test('getTmpDir creates a fresh dir under .tmp/html-effectiveness/', () => {
|
|
8
|
-
const dir = getTmpDir('unit-a');
|
|
9
|
-
assert.ok(dir.endsWith('/.tmp/html-effectiveness/unit-a'));
|
|
10
|
-
assert.ok(existsSync(dir));
|
|
11
|
-
cleanup('unit-a');
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
test('getTmpDir wipes existing dir contents', () => {
|
|
15
|
-
const dir = getTmpDir('unit-b');
|
|
16
|
-
writeFileSync(join(dir, 'old.txt'), 'stale');
|
|
17
|
-
const dir2 = getTmpDir('unit-b');
|
|
18
|
-
assert.equal(dir, dir2);
|
|
19
|
-
assert.equal(existsSync(join(dir2, 'old.txt')), false);
|
|
20
|
-
cleanup('unit-b');
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test('cleanup removes dir', () => {
|
|
24
|
-
const dir = getTmpDir('unit-c');
|
|
25
|
-
cleanup('unit-c');
|
|
26
|
-
assert.equal(existsSync(dir), false);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test('cleanup is idempotent on missing dir', () => {
|
|
30
|
-
cleanup('unit-never-created');
|
|
31
|
-
assert.ok(true);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test('withTmp runs callback and cleans up on success', () => {
|
|
35
|
-
let observed;
|
|
36
|
-
const result = withTmp('unit-d', (dir) => {
|
|
37
|
-
observed = dir;
|
|
38
|
-
writeFileSync(join(dir, 'x'), 'y');
|
|
39
|
-
return 'done';
|
|
40
|
-
});
|
|
41
|
-
assert.equal(result, 'done');
|
|
42
|
-
assert.equal(existsSync(observed), false);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test('withTmp cleans up even when callback throws', () => {
|
|
46
|
-
let observed;
|
|
47
|
-
assert.throws(() => {
|
|
48
|
-
withTmp('unit-e', (dir) => {
|
|
49
|
-
observed = dir;
|
|
50
|
-
throw new Error('boom');
|
|
51
|
-
});
|
|
52
|
-
}, /boom/);
|
|
53
|
-
assert.equal(existsSync(observed), false);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test('ROOT is anchored at repo .tmp/html-effectiveness', () => {
|
|
57
|
-
assert.ok(ROOT.endsWith('/.tmp/html-effectiveness'));
|
|
58
|
-
});
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"title": "Engineering Status",
|
|
3
|
-
"auto_pill": "Auto-generated",
|
|
4
|
-
"date_range": "Apr 29 — May 13, 2026",
|
|
5
|
-
"repo": "drunkcoding/agents-and-skills",
|
|
6
|
-
"summary": [
|
|
7
|
-
{ "num": "12", "label": "Shipped", "delta": "+3 vs prev", "delta_kind": "up" },
|
|
8
|
-
{ "num": "47", "label": "Open bugs", "delta": "-8", "delta_kind": "down" },
|
|
9
|
-
{ "num": "14", "label": "On-call", "delta": "+2", "delta_kind": "up", "warn": true },
|
|
10
|
-
{ "num": "92%","label": "CI green", "delta": "flat", "delta_kind": "flat" }
|
|
11
|
-
],
|
|
12
|
-
"highlights": {
|
|
13
|
-
"items": [
|
|
14
|
-
{ "strong": "Auth v2 cutover.", "body": "All traffic on new identity stack; p95 latency down <em>30%</em>." },
|
|
15
|
-
{ "strong": "Search reindex.", "body": "Full corpus rebuilt in 11h, down from 26h." },
|
|
16
|
-
{ "strong": "Mobile beta.", "body": "iOS 17 build live to internal testers." }
|
|
17
|
-
]
|
|
18
|
-
},
|
|
19
|
-
"shipped": {
|
|
20
|
-
"rows": [
|
|
21
|
-
{ "pr": "#1042", "title": "feat(auth): rotate session keys", "author": "alice", "risk": "low" },
|
|
22
|
-
{ "pr": "#1048", "title": "fix(search): off-by-one in date filter", "author": "bob", "risk": "low" },
|
|
23
|
-
{ "pr": "#1055", "title": "perf(api): cache permission lookups", "author": "charlie", "risk": "med" },
|
|
24
|
-
{ "pr": "#1061", "title": "feat(mobile): push notif opt-in", "author": "dana", "risk": "high" }
|
|
25
|
-
]
|
|
26
|
-
},
|
|
27
|
-
"velocity_svg": "<svg viewBox=\"0 0 600 160\" xmlns=\"http://www.w3.org/2000/svg\"><polyline fill=\"none\" stroke=\"#D97757\" stroke-width=\"2\" points=\"0,120 75,90 150,100 225,70 300,60 375,80 450,40 525,30 600,50\"/><line x1=\"0\" y1=\"150\" x2=\"600\" y2=\"150\" stroke=\"#D1CFC5\"/></svg>",
|
|
28
|
-
"velocity_caption": "Merged PRs per day, 2-week window. Source: GitHub API.",
|
|
29
|
-
"carryover": {
|
|
30
|
-
"items": [
|
|
31
|
-
{ "tag": "BLOCKED", "body": "App Store review for iOS 17 build.", "who": "dana" },
|
|
32
|
-
{ "tag": "REVIEW", "body": "Permission cache invalidation edge case.", "who": "charlie" },
|
|
33
|
-
{ "tag": "SCOPE", "body": "Q3 platform RFC pending review.", "who": "alice" }
|
|
34
|
-
]
|
|
35
|
-
},
|
|
36
|
-
"footer": "Generated 2026-05-13 · <a href=\"#\">runbook</a>"
|
|
37
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"_meta": { "upstream_repo": "test", "upstream_sha": "test", "extracted_at": "test" },
|
|
3
|
-
"_canned": {
|
|
4
|
-
"title": "Canned",
|
|
5
|
-
"use_cases": ["test"],
|
|
6
|
-
"pattern": "test",
|
|
7
|
-
"tmpl": "fixtures/_canned.html.tmpl",
|
|
8
|
-
"slots": {
|
|
9
|
-
"title": { "type": "string", "required": true },
|
|
10
|
-
"items": {
|
|
11
|
-
"type": "array",
|
|
12
|
-
"of": { "name": "string", "html": "html" }
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
"asset_bundles": [],
|
|
16
|
-
"extra_js": null
|
|
17
|
-
}
|
|
18
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
-
import { dirname, join } from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
|
|
7
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const PLUGIN_ROOT = dirname(HERE);
|
|
9
|
-
const MANIFEST = JSON.parse(readFileSync(join(PLUGIN_ROOT, 'templates/manifest.json'), 'utf8'));
|
|
10
|
-
const VALID_BUNDLES = new Set(['base', 'components', 'charts']);
|
|
11
|
-
|
|
12
|
-
const entries = Object.entries(MANIFEST).filter(([k]) => k !== '_meta');
|
|
13
|
-
|
|
14
|
-
test('manifest declares 20 vendored templates', () => {
|
|
15
|
-
assert.equal(entries.length, 20, `expected 20 entries, got ${entries.length}`);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test('_meta records upstream SHA and fetch date', () => {
|
|
19
|
-
const meta = MANIFEST._meta;
|
|
20
|
-
assert.ok(meta, '_meta missing');
|
|
21
|
-
assert.match(meta.upstream_sha, /^[0-9a-f]{40}$/, 'upstream_sha must be 40-char hex');
|
|
22
|
-
assert.match(meta.fetched_at, /^\d{4}-\d{2}-\d{2}$/, 'fetched_at must be YYYY-MM-DD');
|
|
23
|
-
assert.equal(meta.upstream_repo, 'ThariqS/html-effectiveness');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
for (const [id, entry] of entries) {
|
|
27
|
-
test(`${id}: required fields present`, () => {
|
|
28
|
-
for (const f of ['title', 'use_cases', 'pattern', 'slots', 'asset_bundles']) {
|
|
29
|
-
assert.ok(f in entry, `${id} missing field "${f}"`);
|
|
30
|
-
}
|
|
31
|
-
assert.ok(Array.isArray(entry.use_cases), `${id} use_cases must be array`);
|
|
32
|
-
assert.ok(entry.use_cases.length > 0, `${id} use_cases must be non-empty`);
|
|
33
|
-
assert.equal(typeof entry.pattern, 'string', `${id} pattern must be string`);
|
|
34
|
-
assert.equal(typeof entry.slots, 'object', `${id} slots must be object`);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test(`${id}: asset_bundles valid`, () => {
|
|
38
|
-
assert.ok(Array.isArray(entry.asset_bundles), `${id} asset_bundles not array`);
|
|
39
|
-
for (const b of entry.asset_bundles) {
|
|
40
|
-
assert.ok(VALID_BUNDLES.has(b), `${id} asset_bundles contains invalid "${b}"`);
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test(`${id}: template file exists`, () => {
|
|
45
|
-
const tmplRel = entry.tmpl || `templates/${id}.html.tmpl`;
|
|
46
|
-
const tmplAbs = join(PLUGIN_ROOT, tmplRel);
|
|
47
|
-
assert.ok(existsSync(tmplAbs), `${id} template file missing: ${tmplRel}`);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test(`${id}: fixture file exists`, () => {
|
|
51
|
-
const fix = join(PLUGIN_ROOT, `tests/fixtures/${id}.data.json`);
|
|
52
|
-
assert.ok(existsSync(fix), `${id} fixture missing: tests/fixtures/${id}.data.json`);
|
|
53
|
-
const data = JSON.parse(readFileSync(fix, 'utf8'));
|
|
54
|
-
for (const [slot, schemaRaw] of Object.entries(entry.slots)) {
|
|
55
|
-
const schema = typeof schemaRaw === 'string' ? { type: schemaRaw } : schemaRaw;
|
|
56
|
-
if (schema.required) {
|
|
57
|
-
assert.ok(slot in data, `${id} fixture missing required slot "${slot}"`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { render } from '../scripts/mustache.js';
|
|
4
|
-
|
|
5
|
-
test('renders bare variable, escaped by default', () => {
|
|
6
|
-
assert.equal(render('Hello {{name}}', { name: 'World' }), 'Hello World');
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
test('escapes HTML special chars in {{var}}', () => {
|
|
10
|
-
assert.equal(
|
|
11
|
-
render('{{x}}', { x: '<script>alert(1)</script>' }),
|
|
12
|
-
'<script>alert(1)</script>'
|
|
13
|
-
);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test('triple-stash {{{raw}}} bypasses escaping', () => {
|
|
17
|
-
assert.equal(render('{{{html}}}', { html: '<b>x</b>' }), '<b>x</b>');
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
test('section iterates array with sub-context', () => {
|
|
21
|
-
const tmpl = '{{#items}}[{{name}}]{{/items}}';
|
|
22
|
-
assert.equal(render(tmpl, { items: [{ name: 'a' }, { name: 'b' }] }), '[a][b]');
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test('inverted section renders only when falsy/empty', () => {
|
|
26
|
-
assert.equal(render('{{^items}}empty{{/items}}', { items: [] }), 'empty');
|
|
27
|
-
assert.equal(render('{{^items}}empty{{/items}}', { items: [{}] }), '');
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test('missing key renders as empty string, not "undefined"', () => {
|
|
31
|
-
assert.equal(render('x={{missing}}', {}), 'x=');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test('nested sections respect inherited context', () => {
|
|
35
|
-
const tmpl = '{{#a}}{{#b}}{{x}}{{/b}}{{/a}}';
|
|
36
|
-
assert.equal(render(tmpl, { a: { b: [{ x: 'ok' }] } }), 'ok');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test('falsy section (false, null, undefined, empty array) renders nothing', () => {
|
|
40
|
-
for (const val of [false, null, undefined, []]) {
|
|
41
|
-
assert.equal(render('{{#x}}body{{/x}}', { x: val }), '');
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test('preserves literal text around tags', () => {
|
|
46
|
-
assert.equal(render('a {{x}} b', { x: '1' }), 'a 1 b');
|
|
47
|
-
});
|