@drunkcoding/agents-and-skills 0.0.17 → 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.
Files changed (58) hide show
  1. package/.claude-plugin/marketplace.json +19 -4
  2. package/README.md +24 -0
  3. package/package.json +1 -1
  4. package/plugins/auto-power/.claude-plugin/plugin.json +17 -0
  5. package/plugins/auto-power/README.md +80 -0
  6. package/plugins/auto-power/assets/CHECKPOINT_SCHEMA.md +69 -0
  7. package/plugins/auto-power/assets/ESCALATION_TEMPLATE.md +67 -0
  8. package/plugins/auto-power/commands/auto-power-resume.md +32 -0
  9. package/plugins/auto-power/commands/auto-power.md +46 -0
  10. package/plugins/auto-power/skills/auto-power-runtime/SKILL.md +220 -0
  11. package/plugins/html-effectiveness/.claude-plugin/plugin.json +1 -1
  12. package/plugins/plugin-validator/.claude-plugin/plugin.json +1 -1
  13. package/plugins/team-superpower/.claude-plugin/plugin.json +1 -1
  14. package/plugins/team-superpower/agents/backend-developer.md +93 -3
  15. package/plugins/team-superpower/agents/designer.md +17 -2
  16. package/plugins/team-superpower/agents/frontend-developer.md +94 -3
  17. package/plugins/team-superpower/agents/planner.md +62 -6
  18. package/plugins/team-superpower/agents/qa-engineer.md +73 -17
  19. package/plugins/team-superpower/agents/reviewer.md +71 -6
  20. package/plugins/team-superpower/agents/security-engineer.md +16 -1
  21. package/plugins/team-superpower/agents/software-architect.md +16 -1
  22. package/plugins/team-superpower/assets/AGENTS.md.template +23 -0
  23. package/plugins/team-superpower/assets/CLAUDE.md.template +17 -0
  24. package/plugins/team-superpower/assets/SESSION_README.md +47 -0
  25. package/plugins/team-superpower/commands/team-feature.md +217 -11
  26. package/plugins/team-superpower/hooks/task-completed.sh +203 -0
  27. package/plugins/team-superpower/hooks/task-created.sh +42 -1
  28. package/plugins/team-superpower/scripts/assess-complexity.sh +194 -0
  29. package/plugins/team-superpower/scripts/detect-stack.sh +39 -0
  30. package/plugins/team-superpower/scripts/wave-collision-check.sh +60 -0
  31. package/plugins/tech-graph/.claude-plugin/plugin.json +1 -1
  32. package/plugins/html-effectiveness/tests/_tmp.test.js +0 -58
  33. package/plugins/html-effectiveness/tests/fixtures/01-exploration-code-approaches.data.json +0 -3
  34. package/plugins/html-effectiveness/tests/fixtures/02-exploration-visual-designs.data.json +0 -3
  35. package/plugins/html-effectiveness/tests/fixtures/03-code-review-pr.data.json +0 -3
  36. package/plugins/html-effectiveness/tests/fixtures/04-code-understanding.data.json +0 -3
  37. package/plugins/html-effectiveness/tests/fixtures/05-design-system.data.json +0 -3
  38. package/plugins/html-effectiveness/tests/fixtures/06-component-variants.data.json +0 -3
  39. package/plugins/html-effectiveness/tests/fixtures/07-prototype-animation.data.json +0 -3
  40. package/plugins/html-effectiveness/tests/fixtures/08-prototype-interaction.data.json +0 -3
  41. package/plugins/html-effectiveness/tests/fixtures/09-slide-deck.data.json +0 -3
  42. package/plugins/html-effectiveness/tests/fixtures/10-svg-illustrations.data.json +0 -3
  43. package/plugins/html-effectiveness/tests/fixtures/11-status-report.data.json +0 -37
  44. package/plugins/html-effectiveness/tests/fixtures/12-incident-report.data.json +0 -3
  45. package/plugins/html-effectiveness/tests/fixtures/13-flowchart-diagram.data.json +0 -3
  46. package/plugins/html-effectiveness/tests/fixtures/14-research-feature-explainer.data.json +0 -3
  47. package/plugins/html-effectiveness/tests/fixtures/15-research-concept-explainer.data.json +0 -3
  48. package/plugins/html-effectiveness/tests/fixtures/16-implementation-plan.data.json +0 -3
  49. package/plugins/html-effectiveness/tests/fixtures/17-pr-writeup.data.json +0 -3
  50. package/plugins/html-effectiveness/tests/fixtures/18-editor-triage-board.data.json +0 -3
  51. package/plugins/html-effectiveness/tests/fixtures/19-editor-feature-flags.data.json +0 -3
  52. package/plugins/html-effectiveness/tests/fixtures/20-editor-prompt-tuner.data.json +0 -3
  53. package/plugins/html-effectiveness/tests/fixtures/_canned.data.json +0 -7
  54. package/plugins/html-effectiveness/tests/fixtures/_canned.html.tmpl +0 -7
  55. package/plugins/html-effectiveness/tests/fixtures/_canned.manifest.json +0 -18
  56. package/plugins/html-effectiveness/tests/manifest.test.js +0 -61
  57. package/plugins/html-effectiveness/tests/mustache.test.js +0 -47
  58. package/plugins/html-effectiveness/tests/render.test.js +0 -118
@@ -53,6 +53,209 @@ case "$title" in
53
53
  ;;
54
54
  esac
55
55
 
56
+ # v3: wave metadata. impl:* tasks must carry a `wave:` integer (planner sets
57
+ # it; the lead mirrors it into shared-task-list metadata at dispatch time).
58
+ case "$title" in
59
+ impl:*)
60
+ wave="$(printf '%s' "$payload" | jq -r '.task.metadata.wave // .metadata.wave // ""' 2>/dev/null || echo "")"
61
+ if [ -z "$wave" ] || ! printf '%s' "$wave" | grep -qE '^[0-9]+$'; then
62
+ printf '{"ts":"%s","hook":"task-completed","warn":"MISSING_WAVE_METADATA","title":%s}\n' \
63
+ "$ts" "$(printf '%s' "$title" | jq -Rs .)" >> "$LOG_FILE"
64
+ fi
65
+ ;;
66
+ esac
67
+
68
+ # v3: MAX_ITERATIONS guardrail. impl:* completions whose iteration_count
69
+ # exceeds the per-project cap (default 8) must carry a reflection: block.
70
+ case "$title" in
71
+ impl:*)
72
+ iteration_count="$(printf '%s' "$payload" | jq -r '.task.metadata.iteration_count // .metadata.iteration_count // 0' 2>/dev/null || echo 0)"
73
+ reflection="$(printf '%s' "$payload" | jq -r '.task.metadata.reflection // .metadata.reflection // ""' 2>/dev/null || echo "")"
74
+
75
+ cap=8
76
+ parse_helper="${CLAUDE_PLUGIN_ROOT:-}/scripts/parse-claudemd.sh"
77
+ if [ -z "${CLAUDE_PLUGIN_ROOT:-}" ]; then
78
+ # Fall back to a relative path the hook can find when CLAUDE_PLUGIN_ROOT is unset.
79
+ hook_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
80
+ parse_helper="$hook_dir/../scripts/parse-claudemd.sh"
81
+ fi
82
+ if [ -f "$parse_helper" ] && [ -f "${CLAUDE_PROJECT_DIR:-$PWD}/CLAUDE.md" ]; then
83
+ configured="$(bash "$parse_helper" get limits.max_iterations_per_task "${CLAUDE_PROJECT_DIR:-$PWD}/CLAUDE.md" 2>/dev/null || true)"
84
+ if [ -n "$configured" ] && printf '%s' "$configured" | grep -qE '^[0-9]+$'; then
85
+ cap="$configured"
86
+ fi
87
+ fi
88
+
89
+ if [ "${iteration_count:-0}" -gt "$cap" ] && [ -z "$reflection" ]; then
90
+ printf '{"ts":"%s","hook":"task-completed","warn":"ITERATION_CAP_EXCEEDED","title":%s,"iteration_count":%d,"cap":%d}\n' \
91
+ "$ts" \
92
+ "$(printf '%s' "$title" | jq -Rs .)" \
93
+ "$iteration_count" \
94
+ "$cap" \
95
+ >> "$LOG_FILE"
96
+ fi
97
+ ;;
98
+ esac
99
+
100
+ # v4: per-task QA verification. impl:* completions must carry
101
+ # `qa_verified_at:` metadata AND a `QA-verified: round=N` line in at least one
102
+ # commit message body (N ≤ max_qa_rounds_per_task, default 3). The hook also
103
+ # rejects `trivial=true` claims for diffs >20 lines or new-file additions, and
104
+ # rejects `qa_rounds: 3` escalations missing the cross-role fields.
105
+ case "$title" in
106
+ impl:*)
107
+ qa_verified_at="$(printf '%s' "$payload" | jq -r '.task.metadata.qa_verified_at // .metadata.qa_verified_at // ""' 2>/dev/null || echo "")"
108
+ qa_rounds="$(printf '%s' "$payload" | jq -r '.task.metadata.qa_rounds // .metadata.qa_rounds // 0' 2>/dev/null || echo 0)"
109
+ trivial_claim="$(printf '%s' "$payload" | jq -r '.task.metadata.trivial // .metadata.trivial // false' 2>/dev/null || echo false)"
110
+
111
+ qa_cap=3
112
+ if [ -f "$parse_helper" ] && [ -f "${CLAUDE_PROJECT_DIR:-$PWD}/CLAUDE.md" ]; then
113
+ configured_qa="$(bash "$parse_helper" get limits.max_qa_rounds_per_task "${CLAUDE_PROJECT_DIR:-$PWD}/CLAUDE.md" 2>/dev/null || true)"
114
+ if [ -n "$configured_qa" ] && printf '%s' "$configured_qa" | grep -qE '^[0-9]+$'; then
115
+ qa_cap="$configured_qa"
116
+ fi
117
+ fi
118
+
119
+ # Check 7a: qa_verified_at metadata present.
120
+ if [ -z "$qa_verified_at" ]; then
121
+ printf '{"ts":"%s","hook":"task-completed","warn":"MISSING_QA_VERIFICATION","title":%s,"reason":"qa_verified_at metadata missing"}\n' \
122
+ "$ts" "$(printf '%s' "$title" | jq -Rs .)" >> "$LOG_FILE"
123
+ fi
124
+
125
+ # Check 7b: QA-verified: round=N commit body line, verified via git log if commits recorded.
126
+ if command -v git >/dev/null 2>&1; then
127
+ qa_commits="$(printf '%s' "$payload" | jq -r '(.task.metadata.commits // .metadata.commits // [])[]?' 2>/dev/null || true)"
128
+ if [ -n "$qa_commits" ]; then
129
+ qa_line_found=0
130
+ qa_round_observed=0
131
+ while IFS= read -r sha; do
132
+ [ -z "$sha" ] && continue
133
+ body="$(git show --no-color --no-patch --format=%B "$sha" 2>/dev/null || true)"
134
+ line="$(printf '%s\n' "$body" | grep -E '^QA-verified:[[:space:]]*round=[0-9]+' | head -n1 || true)"
135
+ if [ -n "$line" ]; then
136
+ qa_line_found=1
137
+ n="$(printf '%s' "$line" | sed -nE 's/^QA-verified:[[:space:]]*round=([0-9]+).*/\1/p')"
138
+ if [ -n "$n" ] && [ "$n" -gt "$qa_round_observed" ]; then
139
+ qa_round_observed="$n"
140
+ fi
141
+ fi
142
+ done <<< "$qa_commits"
143
+ if [ "$qa_line_found" -ne 1 ]; then
144
+ printf '{"ts":"%s","hook":"task-completed","warn":"MISSING_QA_VERIFICATION","title":%s,"reason":"no QA-verified line in commit body"}\n' \
145
+ "$ts" "$(printf '%s' "$title" | jq -Rs .)" >> "$LOG_FILE"
146
+ elif [ "$qa_round_observed" -gt "$qa_cap" ]; then
147
+ printf '{"ts":"%s","hook":"task-completed","warn":"QA_ROUND_OVER_CAP","title":%s,"round":%d,"cap":%d}\n' \
148
+ "$ts" "$(printf '%s' "$title" | jq -Rs .)" "$qa_round_observed" "$qa_cap" >> "$LOG_FILE"
149
+ fi
150
+ fi
151
+ fi
152
+
153
+ # Check 8: qa_rounds at cap requires §7 cross-role escalation.
154
+ if [ "${qa_rounds:-0}" -ge "$qa_cap" ] && [ -z "$qa_verified_at" ]; then
155
+ # An escalation entry tagged class:cross-role with qa_rounds, what_failed, one_change_to_fix must be present.
156
+ escalation_ok=0
157
+ escalation_entries="$(printf '%s' "$payload" | jq -c '(.task.metadata.blocked_questions // .metadata.blocked_questions // [])[]?' 2>/dev/null || true)"
158
+ if [ -n "$escalation_entries" ]; then
159
+ while IFS= read -r entry; do
160
+ [ -z "$entry" ] && continue
161
+ raw="$(printf '%s' "$entry" | jq -r '.' 2>/dev/null || printf '%s' "$entry")"
162
+ if printf '%s' "$raw" | grep -qE 'class[[:space:]]*:[[:space:]]*cross-role' \
163
+ && printf '%s' "$raw" | grep -qE 'qa_rounds[[:space:]]*:' \
164
+ && printf '%s' "$raw" | grep -qE 'what_failed[[:space:]]*:' \
165
+ && printf '%s' "$raw" | grep -qE 'one_change_to_fix[[:space:]]*:'; then
166
+ escalation_ok=1
167
+ break
168
+ fi
169
+ done <<< "$escalation_entries"
170
+ fi
171
+ if [ "$escalation_ok" -ne 1 ]; then
172
+ printf '{"ts":"%s","hook":"task-completed","warn":"QA_CAP_EXCEEDED","title":%s,"qa_rounds":%d,"cap":%d}\n' \
173
+ "$ts" "$(printf '%s' "$title" | jq -Rs .)" "$qa_rounds" "$qa_cap" >> "$LOG_FILE"
174
+ fi
175
+ fi
176
+
177
+ # Pre-check: INVALID_TRIVIAL_CLAIM — trivial=true requires diff ≤20 lines AND no new files.
178
+ if [ "$trivial_claim" = "true" ] && command -v git >/dev/null 2>&1; then
179
+ tc_commits="$(printf '%s' "$payload" | jq -r '(.task.metadata.commits // .metadata.commits // [])[]?' 2>/dev/null || true)"
180
+ tc_added=0
181
+ tc_lines=0
182
+ if [ -n "$tc_commits" ]; then
183
+ while IFS= read -r sha; do
184
+ [ -z "$sha" ] && continue
185
+ st="$(git show --no-color --name-status --pretty=format: "$sha" 2>/dev/null || true)"
186
+ if printf '%s\n' "$st" | grep -qE '^A[[:space:]]'; then
187
+ tc_added=1
188
+ fi
189
+ n="$(git show --no-color --shortstat --pretty=format: "$sha" 2>/dev/null | grep -oE '[0-9]+ insertion' | head -n1 | grep -oE '[0-9]+' || echo 0)"
190
+ tc_lines=$((tc_lines + ${n:-0}))
191
+ done <<< "$tc_commits"
192
+ if [ "$tc_added" -eq 1 ] || [ "$tc_lines" -gt 20 ]; then
193
+ printf '{"ts":"%s","hook":"task-completed","warn":"INVALID_TRIVIAL_CLAIM","title":%s,"added_files":%d,"insertions":%d}\n' \
194
+ "$ts" "$(printf '%s' "$title" | jq -Rs .)" "$tc_added" "$tc_lines" >> "$LOG_FILE"
195
+ fi
196
+ fi
197
+ fi
198
+ ;;
199
+ esac
200
+
201
+ # v4: retrieval budget. impl:* tasks may not exceed `retrieval_budget_per_task`
202
+ # (default 2). Also: a `Flagged-assumptions:` line in a commit body is only
203
+ # permitted when retrieval_requests == budget (cannot flag assumptions
204
+ # prematurely without exhausting the budget).
205
+ case "$title" in
206
+ impl:*)
207
+ retrieval_requests="$(printf '%s' "$payload" | jq -r '.task.metadata.retrieval_requests // .metadata.retrieval_requests // 0' 2>/dev/null || echo 0)"
208
+ retrieval_cap=2
209
+ if [ -f "$parse_helper" ] && [ -f "${CLAUDE_PROJECT_DIR:-$PWD}/CLAUDE.md" ]; then
210
+ configured_r="$(bash "$parse_helper" get limits.retrieval_budget_per_task "${CLAUDE_PROJECT_DIR:-$PWD}/CLAUDE.md" 2>/dev/null || true)"
211
+ if [ -n "$configured_r" ] && printf '%s' "$configured_r" | grep -qE '^[0-9]+$'; then
212
+ retrieval_cap="$configured_r"
213
+ fi
214
+ fi
215
+
216
+ if [ "${retrieval_requests:-0}" -gt "$retrieval_cap" ]; then
217
+ printf '{"ts":"%s","hook":"task-completed","warn":"RETRIEVAL_BUDGET_EXCEEDED","title":%s,"requests":%d,"cap":%d}\n' \
218
+ "$ts" "$(printf '%s' "$title" | jq -Rs .)" "$retrieval_requests" "$retrieval_cap" >> "$LOG_FILE"
219
+ fi
220
+
221
+ if command -v git >/dev/null 2>&1; then
222
+ r_commits="$(printf '%s' "$payload" | jq -r '(.task.metadata.commits // .metadata.commits // [])[]?' 2>/dev/null || true)"
223
+ if [ -n "$r_commits" ]; then
224
+ flagged=0
225
+ while IFS= read -r sha; do
226
+ [ -z "$sha" ] && continue
227
+ body="$(git show --no-color --no-patch --format=%B "$sha" 2>/dev/null || true)"
228
+ if printf '%s\n' "$body" | grep -qE '^Flagged-assumptions:'; then
229
+ flagged=1; break
230
+ fi
231
+ done <<< "$r_commits"
232
+ if [ "$flagged" -eq 1 ] && [ "${retrieval_requests:-0}" -lt "$retrieval_cap" ]; then
233
+ printf '{"ts":"%s","hook":"task-completed","warn":"PREMATURE_ASSUMPTION_FLAG","title":%s,"requests":%d,"cap":%d}\n' \
234
+ "$ts" "$(printf '%s' "$title" | jq -Rs .)" "$retrieval_requests" "$retrieval_cap" >> "$LOG_FILE"
235
+ fi
236
+ fi
237
+ fi
238
+ ;;
239
+ esac
240
+
241
+ # v4: AGENTS.md protection. Agents never write to docs/superpowers/AGENTS.md.
242
+ # Reviewer writes only to docs/superpowers/AGENTS.suggestions.md. If any commit
243
+ # on this task touches AGENTS.md, warn AGENT_WROTE_AGENTS_MD — the owner is the
244
+ # only role that may promote suggestions to AGENTS.md.
245
+ if command -v git >/dev/null 2>&1; then
246
+ agm_commits="$(printf '%s' "$payload" | jq -r '(.task.metadata.commits // .metadata.commits // [])[]?' 2>/dev/null || true)"
247
+ if [ -n "$agm_commits" ]; then
248
+ while IFS= read -r sha; do
249
+ [ -z "$sha" ] && continue
250
+ files="$(git show --no-color --name-only --pretty=format: "$sha" 2>/dev/null || true)"
251
+ if printf '%s\n' "$files" | grep -qE '^docs/superpowers/AGENTS\.md$'; then
252
+ printf '{"ts":"%s","hook":"task-completed","warn":"AGENT_WROTE_AGENTS_MD","title":%s,"sha":%s}\n' \
253
+ "$ts" "$(printf '%s' "$title" | jq -Rs .)" "$(printf '%s' "$sha" | jq -Rs .)" >> "$LOG_FILE"
254
+ fi
255
+ done <<< "$agm_commits"
256
+ fi
257
+ fi
258
+
56
259
  # v2: migration serialization. If this is a migration task, verify no other
57
260
  # `impl:be-migration-*` task is currently in_progress in the shared task list
58
261
  # (payload.tasks[]). The lead also enforces this; the hook is a backstop.
@@ -59,10 +59,17 @@ if [ -d "$SESSIONS_DIR" ]; then
59
59
  fi
60
60
  fi
61
61
 
62
- printf '{"ts":"%s","hook":"task-created","title":%s,"shape":%s}\n' \
62
+ wave="$(printf '%s' "$payload" | jq -r '.task.metadata.wave // .metadata.wave // ""' 2>/dev/null || echo "")"
63
+ wave_json="null"
64
+ if printf '%s' "$wave" | grep -qE '^[0-9]+$'; then
65
+ wave_json="$wave"
66
+ fi
67
+
68
+ printf '{"ts":"%s","hook":"task-created","title":%s,"shape":%s,"wave":%s}\n' \
63
69
  "$ts" \
64
70
  "$(printf '%s' "$title" | jq -Rs .)" \
65
71
  "$(printf '%s' "$shape" | jq -Rs .)" \
72
+ "$wave_json" \
66
73
  >> "$LOG_FILE"
67
74
 
68
75
  # Top-level prefix check
@@ -77,6 +84,40 @@ case "$title" in
77
84
  ;;
78
85
  esac
79
86
 
87
+ # v3: solo-mode guard. If checkpoint mode is solo, impl:* tasks are invalid
88
+ # (solo means the lead does the work itself; no implementer is spawned).
89
+ mode_meta="$(printf '%s' "$payload" | jq -r '.task.metadata.mode // .metadata.mode // ""' 2>/dev/null || echo "")"
90
+ mode_marker=""
91
+ if [ -z "$mode_meta" ] && [ -d "$SESSIONS_DIR" ]; then
92
+ # Look for a <slug>.mode file the lead writes when it picks a mode.
93
+ marker_file=""
94
+ if [ -n "$slug" ] && [ -f "$SESSIONS_DIR/$slug.mode" ]; then
95
+ marker_file="$SESSIONS_DIR/$slug.mode"
96
+ else
97
+ count="$(find "$SESSIONS_DIR" -maxdepth 1 -type f -name '*.mode' 2>/dev/null | wc -l | tr -d ' ')"
98
+ if [ "$count" = "1" ]; then
99
+ marker_file="$(find "$SESSIONS_DIR" -maxdepth 1 -type f -name '*.mode' 2>/dev/null | head -n1)"
100
+ fi
101
+ fi
102
+ if [ -n "$marker_file" ] && [ -f "$marker_file" ]; then
103
+ mode_marker="$(head -n1 "$marker_file" | tr -d '[:space:]')"
104
+ fi
105
+ fi
106
+ effective_mode="${mode_meta:-$mode_marker}"
107
+
108
+ case "$title" in
109
+ impl:*)
110
+ if [ "$effective_mode" = "solo" ]; then
111
+ printf '{"ts":"%s","hook":"task-created","warn":"INVALID_FOR_SOLO_MODE","title":%s}\n' \
112
+ "$ts" "$(printf '%s' "$title" | jq -Rs .)" >> "$LOG_FILE"
113
+ fi
114
+ if [ -z "$wave" ]; then
115
+ printf '{"ts":"%s","hook":"task-created","warn":"MISSING_WAVE_METADATA","title":%s}\n' \
116
+ "$ts" "$(printf '%s' "$title" | jq -Rs .)" >> "$LOG_FILE"
117
+ fi
118
+ ;;
119
+ esac
120
+
80
121
  # At this point title starts with `impl:`. Strip prefix and require a known
81
122
  # sub-prefix.
82
123
  rest="${title#impl:}"
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "tech-graph",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
4
4
  "description": "Step-by-step wizard for generating technical diagrams as SVG+PNG.",
5
5
  "author": {
6
6
  "name": "steven"
@@ -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,3 +0,0 @@
1
- {
2
- "title": "Code-Approaches Exploration"
3
- }
@@ -1,3 +0,0 @@
1
- {
2
- "title": "Visual Design Exploration"
3
- }
@@ -1,3 +0,0 @@
1
- {
2
- "title": "PR Code Review Board"
3
- }
@@ -1,3 +0,0 @@
1
- {
2
- "title": "Code Walkthrough"
3
- }
@@ -1,3 +0,0 @@
1
- {
2
- "title": "Design System Sheet"
3
- }