@drunkcoding/agents-and-skills 0.0.18 → 0.0.23

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 (62) hide show
  1. package/.claude-plugin/marketplace.json +5 -5
  2. package/README.md +23 -0
  3. package/package.json +1 -1
  4. package/plugins/auto-power/.claude-plugin/plugin.json +1 -1
  5. package/plugins/html-effectiveness/.claude-plugin/plugin.json +1 -1
  6. package/plugins/plugin-validator/.claude-plugin/plugin.json +1 -1
  7. package/plugins/team-superpower/.claude-plugin/plugin.json +1 -1
  8. package/plugins/team-superpower/README.md +186 -115
  9. package/plugins/team-superpower/agents/backend-developer.md +161 -39
  10. package/plugins/team-superpower/agents/feature-planner.md +66 -0
  11. package/plugins/team-superpower/agents/frontend-developer.md +163 -34
  12. package/plugins/team-superpower/agents/orchestrator.md +83 -0
  13. package/plugins/team-superpower/agents/qc-engineer.md +84 -0
  14. package/plugins/team-superpower/agents/security-engineer.md +90 -40
  15. package/plugins/team-superpower/agents/solution-architect.md +80 -0
  16. package/plugins/team-superpower/agents/team-leader.md +100 -0
  17. package/plugins/team-superpower/assets/AGENTS.md.template +23 -0
  18. package/plugins/team-superpower/assets/CLAUDE.md.template +25 -4
  19. package/plugins/team-superpower/assets/ESCALATION.md +114 -66
  20. package/plugins/team-superpower/assets/SESSION_README.md +233 -113
  21. package/plugins/team-superpower/commands/team-feature.md +195 -443
  22. package/plugins/team-superpower/hooks/task-completed.sh +180 -59
  23. package/plugins/team-superpower/hooks/task-created.sh +77 -19
  24. package/plugins/team-superpower/hooks/teammate-idle.sh +118 -13
  25. package/plugins/team-superpower/scripts/assess-complexity.sh +194 -0
  26. package/plugins/team-superpower/scripts/detect-stack.sh +39 -0
  27. package/plugins/team-superpower/scripts/team-state.sh +106 -37
  28. package/plugins/team-superpower/scripts/wave-collision-check.sh +60 -0
  29. package/plugins/tech-graph/.claude-plugin/plugin.json +1 -1
  30. package/plugins/html-effectiveness/tests/_tmp.test.js +0 -58
  31. package/plugins/html-effectiveness/tests/fixtures/01-exploration-code-approaches.data.json +0 -3
  32. package/plugins/html-effectiveness/tests/fixtures/02-exploration-visual-designs.data.json +0 -3
  33. package/plugins/html-effectiveness/tests/fixtures/03-code-review-pr.data.json +0 -3
  34. package/plugins/html-effectiveness/tests/fixtures/04-code-understanding.data.json +0 -3
  35. package/plugins/html-effectiveness/tests/fixtures/05-design-system.data.json +0 -3
  36. package/plugins/html-effectiveness/tests/fixtures/06-component-variants.data.json +0 -3
  37. package/plugins/html-effectiveness/tests/fixtures/07-prototype-animation.data.json +0 -3
  38. package/plugins/html-effectiveness/tests/fixtures/08-prototype-interaction.data.json +0 -3
  39. package/plugins/html-effectiveness/tests/fixtures/09-slide-deck.data.json +0 -3
  40. package/plugins/html-effectiveness/tests/fixtures/10-svg-illustrations.data.json +0 -3
  41. package/plugins/html-effectiveness/tests/fixtures/11-status-report.data.json +0 -37
  42. package/plugins/html-effectiveness/tests/fixtures/12-incident-report.data.json +0 -3
  43. package/plugins/html-effectiveness/tests/fixtures/13-flowchart-diagram.data.json +0 -3
  44. package/plugins/html-effectiveness/tests/fixtures/14-research-feature-explainer.data.json +0 -3
  45. package/plugins/html-effectiveness/tests/fixtures/15-research-concept-explainer.data.json +0 -3
  46. package/plugins/html-effectiveness/tests/fixtures/16-implementation-plan.data.json +0 -3
  47. package/plugins/html-effectiveness/tests/fixtures/17-pr-writeup.data.json +0 -3
  48. package/plugins/html-effectiveness/tests/fixtures/18-editor-triage-board.data.json +0 -3
  49. package/plugins/html-effectiveness/tests/fixtures/19-editor-feature-flags.data.json +0 -3
  50. package/plugins/html-effectiveness/tests/fixtures/20-editor-prompt-tuner.data.json +0 -3
  51. package/plugins/html-effectiveness/tests/fixtures/_canned.data.json +0 -7
  52. package/plugins/html-effectiveness/tests/fixtures/_canned.html.tmpl +0 -7
  53. package/plugins/html-effectiveness/tests/fixtures/_canned.manifest.json +0 -18
  54. package/plugins/html-effectiveness/tests/manifest.test.js +0 -61
  55. package/plugins/html-effectiveness/tests/mustache.test.js +0 -47
  56. package/plugins/html-effectiveness/tests/render.test.js +0 -118
  57. package/plugins/team-superpower/agents/designer.md +0 -50
  58. package/plugins/team-superpower/agents/planner.md +0 -186
  59. package/plugins/team-superpower/agents/qa-engineer.md +0 -47
  60. package/plugins/team-superpower/agents/reviewer.md +0 -110
  61. package/plugins/team-superpower/agents/software-architect.md +0 -45
  62. package/plugins/team-superpower/commands/team-feature-resume.md +0 -185
@@ -1,20 +1,30 @@
1
1
  #!/usr/bin/env bash
2
- # TaskCompleted hook — gate implementation completions on plan approval and
3
- # validate any embedded escalation entries.
2
+ # TaskCompleted hook — v5 gate for implementation completions.
4
3
  #
5
4
  # Accepts a JSON payload on stdin with at minimum:
6
5
  # - task.title: string
7
- # - task.metadata.plan_approved_at: string (ISO datetime), required for impl: tasks
8
- # - task.metadata.blocked_questions: array of strings (optional)
9
- # - task.metadata.commits: array of git SHAs (optional, used by v2 checks)
6
+ # - task.metadata.wave: string (e.g. "1.1", "1.rework", "qc-rework")
7
+ # - task.metadata.iteration_count: integer (optional)
8
+ # - task.metadata.reflection: string (optional, required when iteration_count > cap)
9
+ # - task.metadata.retrieval_requests: integer (optional)
10
+ # - task.metadata.commits: array of git SHAs (optional)
10
11
  # - task.metadata.contract_files: array of paths (optional, for contract-publish)
11
12
  #
12
- # v2 rules added on top of v1:
13
- # - impl:be-migration-* completions: refuse if another in-progress migration
14
- # exists in the shared task list payload (anti-race).
15
- # - impl:be-contract-publish-* completions: at least one commit on this task
16
- # must touch a contracts file (default `contracts/` directory, or paths
17
- # listed in metadata.contract_files). Verified via `git show --name-only`.
13
+ # v5 checks (delta from v4):
14
+ # - REMOVED: per-task QA verification (qa_verified_at, QA-verified commit line, qa_rounds cap, trivial=true claims)
15
+ # - REMOVED: 6-field escalation template validation (Phase/Context/Options/Recommendation/Need from you/Peer attempts)
16
+ # - REMOVED: v4 wave format (single integer); v5 wave is dotted (e.g. "1.1") or rework label
17
+ # - ADDED: MISSING_STATIC_CHECKS every impl:* task must have a .team-superpower/static-check-<task-id>.log
18
+ # containing exit=0 for all sections (lint, format, typecheck).
19
+ # - ADDED: MISSING_REWORK_REFERENCE — every impl:rework-* task must have a `Reworks: <orig-id>` line
20
+ # in at least one commit body.
21
+ #
22
+ # v5 preserved checks:
23
+ # - migration serialization (impl:*-migration-* anti-race)
24
+ # - contract-publish must touch a contracts file
25
+ # - AGENTS.md protection (no agent commit may touch docs/superpowers/AGENTS.md)
26
+ # - retrieval budget cap + premature Flagged-assumptions guard
27
+ # - MAX_ITERATIONS guardrail
18
28
 
19
29
  set -euo pipefail
20
30
 
@@ -37,43 +47,187 @@ if ! command -v jq >/dev/null 2>&1; then
37
47
  fi
38
48
 
39
49
  title="$(printf '%s' "$payload" | jq -r '.task.title // .title // ""' 2>/dev/null || echo "")"
40
- plan_approved_at="$(printf '%s' "$payload" | jq -r '.task.metadata.plan_approved_at // .metadata.plan_approved_at // ""' 2>/dev/null || echo "")"
41
50
 
42
- printf '{"ts":"%s","hook":"task-completed","title":%s,"plan_approved_at":%s}\n' \
51
+ printf '{"ts":"%s","hook":"task-completed","title":%s}\n' \
43
52
  "$ts" \
44
53
  "$(printf '%s' "$title" | jq -Rs .)" \
45
- "$(printf '%s' "$plan_approved_at" | jq -Rs .)" \
46
54
  >> "$LOG_FILE"
47
55
 
56
+ # Resolve parse-claudemd.sh helper for limits.* lookups.
57
+ parse_helper="${CLAUDE_PLUGIN_ROOT:-}/scripts/parse-claudemd.sh"
58
+ if [ -z "${CLAUDE_PLUGIN_ROOT:-}" ]; then
59
+ hook_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
60
+ parse_helper="$hook_dir/../scripts/parse-claudemd.sh"
61
+ fi
62
+
63
+ # v5: wave metadata. impl:* tasks must carry a `wave:` string. Accepted shapes:
64
+ # - "<plan-phase>.<wave>" e.g. "1.1", "2.3"
65
+ # - "<plan-phase>.rework" e.g. "1.rework"
66
+ # - "qc-rework"
48
67
  case "$title" in
49
68
  impl:*)
50
- if [ -z "$plan_approved_at" ]; then
51
- printf '{"ts":"%s","hook":"task-completed","warn":"no_plan_approval","title":%s}\n' "$ts" "$(printf '%s' "$title" | jq -Rs .)" >> "$LOG_FILE"
69
+ wave="$(printf '%s' "$payload" | jq -r '.task.metadata.wave // .metadata.wave // ""' 2>/dev/null || echo "")"
70
+ if [ -z "$wave" ] || ! printf '%s' "$wave" | grep -qE '^([0-9]+\.[0-9]+|[0-9]+\.rework|qc-rework)$'; then
71
+ printf '{"ts":"%s","hook":"task-completed","warn":"MISSING_WAVE_METADATA","title":%s,"wave":%s}\n' \
72
+ "$ts" \
73
+ "$(printf '%s' "$title" | jq -Rs .)" \
74
+ "$(printf '%s' "$wave" | jq -Rs .)" \
75
+ >> "$LOG_FILE"
76
+ fi
77
+ ;;
78
+ esac
79
+
80
+ # v5 check 10: MISSING_STATIC_CHECKS. Every impl:* task must have a
81
+ # .team-superpower/static-check-<task-id>.log with exit=0 across lint/format/typecheck.
82
+ case "$title" in
83
+ impl:*)
84
+ project_dir="${CLAUDE_PROJECT_DIR:-$PWD}"
85
+ static_log="$project_dir/.team-superpower/static-check-${title}.log"
86
+ if [ ! -f "$static_log" ]; then
87
+ printf '{"ts":"%s","hook":"task-completed","warn":"MISSING_STATIC_CHECKS","title":%s,"reason":"log file not found","path":%s}\n' \
88
+ "$ts" \
89
+ "$(printf '%s' "$title" | jq -Rs .)" \
90
+ "$(printf '%s' "$static_log" | jq -Rs .)" \
91
+ >> "$LOG_FILE"
92
+ else
93
+ # Any exit= line with a non-zero value fails the check.
94
+ if grep -E '^exit=' "$static_log" | grep -qvE '^exit=0$'; then
95
+ bad="$(grep -E '^exit=' "$static_log" | grep -vE '^exit=0$' | head -n1 || true)"
96
+ printf '{"ts":"%s","hook":"task-completed","warn":"MISSING_STATIC_CHECKS","title":%s,"reason":"non-zero exit","line":%s}\n' \
97
+ "$ts" \
98
+ "$(printf '%s' "$title" | jq -Rs .)" \
99
+ "$(printf '%s' "$bad" | jq -Rs .)" \
100
+ >> "$LOG_FILE"
101
+ fi
102
+ fi
103
+ ;;
104
+ esac
105
+
106
+ # v5 check 11: MISSING_REWORK_REFERENCE. impl:rework-* tasks must carry a
107
+ # `Reworks: <orig-id-or-qc-issue-id>` line in at least one commit body.
108
+ case "$title" in
109
+ impl:rework-*)
110
+ if command -v git >/dev/null 2>&1; then
111
+ rw_commits="$(printf '%s' "$payload" | jq -r '(.task.metadata.commits // .metadata.commits // [])[]?' 2>/dev/null || true)"
112
+ if [ -n "$rw_commits" ]; then
113
+ rw_found=0
114
+ while IFS= read -r sha; do
115
+ [ -z "$sha" ] && continue
116
+ body="$(git show --no-color --no-patch --format=%B "$sha" 2>/dev/null || true)"
117
+ if printf '%s\n' "$body" | grep -qE '^Reworks:[[:space:]]*[^[:space:]]+'; then
118
+ rw_found=1; break
119
+ fi
120
+ done <<< "$rw_commits"
121
+ if [ "$rw_found" -ne 1 ]; then
122
+ printf '{"ts":"%s","hook":"task-completed","warn":"MISSING_REWORK_REFERENCE","title":%s,"reason":"no Reworks: line in commit body"}\n' \
123
+ "$ts" "$(printf '%s' "$title" | jq -Rs .)" >> "$LOG_FILE"
124
+ fi
125
+ else
126
+ printf '{"ts":"%s","hook":"task-completed","warn":"MISSING_REWORK_REFERENCE","title":%s,"reason":"no commits recorded"}\n' \
127
+ "$ts" "$(printf '%s' "$title" | jq -Rs .)" >> "$LOG_FILE"
128
+ fi
52
129
  fi
53
130
  ;;
54
131
  esac
55
132
 
56
- # v2: migration serialization. If this is a migration task, verify no other
57
- # `impl:be-migration-*` task is currently in_progress in the shared task list
58
- # (payload.tasks[]). The lead also enforces this; the hook is a backstop.
133
+ # MAX_ITERATIONS guardrail (preserved from v3/v4).
59
134
  case "$title" in
60
- impl:be-migration-*)
135
+ impl:*)
136
+ iteration_count="$(printf '%s' "$payload" | jq -r '.task.metadata.iteration_count // .metadata.iteration_count // 0' 2>/dev/null || echo 0)"
137
+ reflection="$(printf '%s' "$payload" | jq -r '.task.metadata.reflection // .metadata.reflection // ""' 2>/dev/null || echo "")"
138
+
139
+ cap=8
140
+ if [ -f "$parse_helper" ] && [ -f "${CLAUDE_PROJECT_DIR:-$PWD}/CLAUDE.md" ]; then
141
+ configured="$(bash "$parse_helper" get limits.max_iterations_per_task "${CLAUDE_PROJECT_DIR:-$PWD}/CLAUDE.md" 2>/dev/null || true)"
142
+ if [ -n "$configured" ] && printf '%s' "$configured" | grep -qE '^[0-9]+$'; then
143
+ cap="$configured"
144
+ fi
145
+ fi
146
+
147
+ if [ "${iteration_count:-0}" -gt "$cap" ] && [ -z "$reflection" ]; then
148
+ printf '{"ts":"%s","hook":"task-completed","warn":"ITERATION_CAP_EXCEEDED","title":%s,"iteration_count":%d,"cap":%d}\n' \
149
+ "$ts" \
150
+ "$(printf '%s' "$title" | jq -Rs .)" \
151
+ "$iteration_count" \
152
+ "$cap" \
153
+ >> "$LOG_FILE"
154
+ fi
155
+ ;;
156
+ esac
157
+
158
+ # Retrieval budget (preserved from v4).
159
+ case "$title" in
160
+ impl:*)
161
+ retrieval_requests="$(printf '%s' "$payload" | jq -r '.task.metadata.retrieval_requests // .metadata.retrieval_requests // 0' 2>/dev/null || echo 0)"
162
+ retrieval_cap=2
163
+ if [ -f "$parse_helper" ] && [ -f "${CLAUDE_PROJECT_DIR:-$PWD}/CLAUDE.md" ]; then
164
+ configured_r="$(bash "$parse_helper" get limits.retrieval_budget_per_task "${CLAUDE_PROJECT_DIR:-$PWD}/CLAUDE.md" 2>/dev/null || true)"
165
+ if [ -n "$configured_r" ] && printf '%s' "$configured_r" | grep -qE '^[0-9]+$'; then
166
+ retrieval_cap="$configured_r"
167
+ fi
168
+ fi
169
+
170
+ if [ "${retrieval_requests:-0}" -gt "$retrieval_cap" ]; then
171
+ printf '{"ts":"%s","hook":"task-completed","warn":"RETRIEVAL_BUDGET_EXCEEDED","title":%s,"requests":%d,"cap":%d}\n' \
172
+ "$ts" "$(printf '%s' "$title" | jq -Rs .)" "$retrieval_requests" "$retrieval_cap" >> "$LOG_FILE"
173
+ fi
174
+
175
+ if command -v git >/dev/null 2>&1; then
176
+ r_commits="$(printf '%s' "$payload" | jq -r '(.task.metadata.commits // .metadata.commits // [])[]?' 2>/dev/null || true)"
177
+ if [ -n "$r_commits" ]; then
178
+ flagged=0
179
+ while IFS= read -r sha; do
180
+ [ -z "$sha" ] && continue
181
+ body="$(git show --no-color --no-patch --format=%B "$sha" 2>/dev/null || true)"
182
+ if printf '%s\n' "$body" | grep -qE '^Flagged-assumptions:'; then
183
+ flagged=1; break
184
+ fi
185
+ done <<< "$r_commits"
186
+ if [ "$flagged" -eq 1 ] && [ "${retrieval_requests:-0}" -lt "$retrieval_cap" ]; then
187
+ printf '{"ts":"%s","hook":"task-completed","warn":"PREMATURE_ASSUMPTION_FLAG","title":%s,"requests":%d,"cap":%d}\n' \
188
+ "$ts" "$(printf '%s' "$title" | jq -Rs .)" "$retrieval_requests" "$retrieval_cap" >> "$LOG_FILE"
189
+ fi
190
+ fi
191
+ fi
192
+ ;;
193
+ esac
194
+
195
+ # AGENTS.md protection (preserved from v4).
196
+ if command -v git >/dev/null 2>&1; then
197
+ agm_commits="$(printf '%s' "$payload" | jq -r '(.task.metadata.commits // .metadata.commits // [])[]?' 2>/dev/null || true)"
198
+ if [ -n "$agm_commits" ]; then
199
+ while IFS= read -r sha; do
200
+ [ -z "$sha" ] && continue
201
+ files="$(git show --no-color --name-only --pretty=format: "$sha" 2>/dev/null || true)"
202
+ if printf '%s\n' "$files" | grep -qE '^docs/superpowers/AGENTS\.md$'; then
203
+ printf '{"ts":"%s","hook":"task-completed","warn":"AGENT_WROTE_AGENTS_MD","title":%s,"sha":%s}\n' \
204
+ "$ts" "$(printf '%s' "$title" | jq -Rs .)" "$(printf '%s' "$sha" | jq -Rs .)" >> "$LOG_FILE"
205
+ fi
206
+ done <<< "$agm_commits"
207
+ fi
208
+ fi
209
+
210
+ # Migration serialization (preserved). v5 widens the prefix match: any task
211
+ # whose title contains the qualifier `-migration-` (e.g. impl:be-migration-*,
212
+ # impl:rework-be-migration-*) is migration-class.
213
+ case "$title" in
214
+ *-migration-*)
61
215
  other_in_progress="$(printf '%s' "$payload" | jq -r --arg me "$title" '
62
216
  [ .tasks[]?
63
217
  | select((.title // "") != $me)
64
- | select(((.title // "") | startswith("impl:be-migration-")))
218
+ | select(((.title // "") | contains("-migration-")))
65
219
  | select((.status // "") == "in_progress")
66
220
  ] | length' 2>/dev/null || echo 0)"
67
221
  if [ "${other_in_progress:-0}" -gt 0 ]; then
68
- printf '{"ts":"%s","hook":"task-completed","warn":"migration_race","title":%s}\n' "$ts" "$(printf '%s' "$title" | jq -Rs .)" >> "$LOG_FILE"
222
+ printf '{"ts":"%s","hook":"task-completed","warn":"MIGRATION_RACE","title":%s}\n' "$ts" "$(printf '%s' "$title" | jq -Rs .)" >> "$LOG_FILE"
69
223
  fi
70
224
  ;;
71
225
  esac
72
226
 
73
- # v2: contract-publish must actually touch a contracts file in at least one
74
- # of its commits. Best-effort: skip if git unavailable or no commits recorded.
227
+ # Contract-publish must touch a contracts file (preserved). v5 widens to any
228
+ # task whose title contains the qualifier `-contract-publish-`.
75
229
  case "$title" in
76
- impl:be-contract-publish-*)
230
+ *-contract-publish-*)
77
231
  if command -v git >/dev/null 2>&1; then
78
232
  commits="$(printf '%s' "$payload" | jq -r '(.task.metadata.commits // .metadata.commits // [])[]?' 2>/dev/null || true)"
79
233
  patterns="$(printf '%s' "$payload" | jq -r '(.task.metadata.contract_files // .metadata.contract_files // ["contracts/"])[]?' 2>/dev/null || echo "contracts/")"
@@ -90,44 +244,11 @@ case "$title" in
90
244
  done <<< "$patterns"
91
245
  done <<< "$commits"
92
246
  if [ "$touched" -ne 1 ]; then
93
- printf '{"ts":"%s","hook":"task-completed","warn":"empty_contract_publish","title":%s}\n' "$ts" "$(printf '%s' "$title" | jq -Rs .)" >> "$LOG_FILE"
247
+ printf '{"ts":"%s","hook":"task-completed","warn":"EMPTY_CONTRACT_PUBLISH","title":%s}\n' "$ts" "$(printf '%s' "$title" | jq -Rs .)" >> "$LOG_FILE"
94
248
  fi
95
249
  fi
96
250
  fi
97
251
  ;;
98
252
  esac
99
253
 
100
- # Validate escalation entries if present.
101
- required_fields=("Phase" "Context" "Options" "Recommendation" "Need from you" "Peer attempts")
102
- missing_any=""
103
- entries="$(printf '%s' "$payload" | jq -c '(.task.metadata.blocked_questions // .metadata.blocked_questions // [])[]?' 2>/dev/null || true)"
104
-
105
- if [ -n "$entries" ]; then
106
- while IFS= read -r entry; do
107
- [ -z "$entry" ] && continue
108
- raw="$(printf '%s' "$entry" | jq -r '.' 2>/dev/null || printf '%s' "$entry")"
109
- missing=""
110
- for field in "${required_fields[@]}"; do
111
- if ! printf '%s' "$raw" | grep -qE "(^|[^A-Za-z])${field}[[:space:]]*:"; then
112
- if [ -z "$missing" ]; then
113
- missing="$field"
114
- else
115
- missing="$missing, $field"
116
- fi
117
- fi
118
- done
119
- if [ -n "$missing" ]; then
120
- if [ -z "$missing_any" ]; then
121
- missing_any="$missing"
122
- else
123
- missing_any="$missing_any | $missing"
124
- fi
125
- fi
126
- done <<< "$entries"
127
- fi
128
-
129
- if [ -n "$missing_any" ]; then
130
- printf '{"ts":"%s","hook":"task-completed","warn":"bad_escalation","missing":%s}\n' "$ts" "$(printf '%s' "$missing_any" | jq -Rs .)" >> "$LOG_FILE"
131
- fi
132
-
133
254
  exit 0
@@ -1,21 +1,28 @@
1
1
  #!/usr/bin/env bash
2
- # TaskCreated hook — enforce title prefix on shared task list entries.
2
+ # TaskCreated hook — v5 prefix + wave validation for shared task list entries.
3
3
  #
4
4
  # Accepts a JSON payload on stdin. Required field:
5
5
  # - task.title: string
6
6
  #
7
- # Title MUST start with one of: impl:, review:, meta:, block:
8
- # v2 additions:
9
- # - impl: tasks MUST carry a sub-prefix (be-, fe-, qa-fix-be-, qa-fix-fe-,
10
- # review-fix-be-, review-fix-fe-, contract-update-, be-migration-,
11
- # be-contract-publish-) bare `impl:foo` is rejected.
12
- # - Shape-marker file at docs/superpowers/sessions/<slug>.shape (written by
13
- # the lead in phase 0) restricts which sub-prefixes are allowed:
14
- # be-only → only `impl:be-*`, `impl:contract-update-*` allowed
15
- # fe-only → only `impl:fe-*`, `impl:contract-update-*` allowed
16
- # full-stack → all sub-prefixes allowed
17
- # - Slug is taken from .task.metadata.slug or .metadata.slug, with fallback
18
- # to the most-recent .shape file when only one exists.
7
+ # v5 changes (delta from v4):
8
+ # - ADDED: impl:rework-* allowed top-level. Rework tasks dispatched by
9
+ # team-leader (phase-end SOLID/DRY review) or qc-engineer (end-of-plan QC)
10
+ # do not carry a be-/fe- prefix; their original-task id is in the
11
+ # `Reworks:` line of the implementer's commit body and validated by
12
+ # task-completed.sh.
13
+ # - ADDED: INVALID_WAVE_REFERENCE v5 waves are dotted strings, not single
14
+ # integers. Accepted shapes: "<plan-phase>.<wave>" (e.g. "1.1", "2.3"),
15
+ # "<plan-phase>.rework" (e.g. "1.rework"), or "qc-rework".
16
+ # - REMOVED: qa-fix-be-, qa-fix-fe-, review-fix-be-, review-fix-fe- prefixes
17
+ # (v4 QA loop is gone). Use impl:rework-* instead.
18
+ # - REMOVED: bare integer wave format.
19
+ #
20
+ # Preserved checks:
21
+ # - Top-level prefix must be impl: / review: / meta: / block:.
22
+ # - impl:* must carry a known sub-prefix per the shape file.
23
+ # - solo-mode guard: impl:* invalid when checkpoint mode is solo.
24
+ # - Shape-marker file at docs/superpowers/sessions/<slug>.shape restricts
25
+ # allowed sub-prefixes (be-only / fe-only / full-stack).
19
26
 
20
27
  set -euo pipefail
21
28
 
@@ -59,10 +66,13 @@ if [ -d "$SESSIONS_DIR" ]; then
59
66
  fi
60
67
  fi
61
68
 
62
- printf '{"ts":"%s","hook":"task-created","title":%s,"shape":%s}\n' \
69
+ wave="$(printf '%s' "$payload" | jq -r '.task.metadata.wave // .metadata.wave // ""' 2>/dev/null || echo "")"
70
+
71
+ printf '{"ts":"%s","hook":"task-created","title":%s,"shape":%s,"wave":%s}\n' \
63
72
  "$ts" \
64
73
  "$(printf '%s' "$title" | jq -Rs .)" \
65
74
  "$(printf '%s' "$shape" | jq -Rs .)" \
75
+ "$(printf '%s' "$wave" | jq -Rs .)" \
66
76
  >> "$LOG_FILE"
67
77
 
68
78
  # Top-level prefix check
@@ -71,20 +81,65 @@ case "$title" in
71
81
  impl:*) ;;
72
82
  "")
73
83
  printf '{"ts":"%s","hook":"task-created","warn":"bad_prefix","reason":"title missing"}\n' "$ts" >> "$LOG_FILE"
84
+ exit 0
74
85
  ;;
75
86
  *)
76
87
  printf '{"ts":"%s","hook":"task-created","warn":"bad_prefix","title":%s}\n' "$ts" "$(printf '%s' "$title" | jq -Rs .)" >> "$LOG_FILE"
88
+ exit 0
77
89
  ;;
78
90
  esac
79
91
 
92
+ # v3: solo-mode guard. If checkpoint mode is solo, impl:* tasks are invalid.
93
+ mode_meta="$(printf '%s' "$payload" | jq -r '.task.metadata.mode // .metadata.mode // ""' 2>/dev/null || echo "")"
94
+ mode_marker=""
95
+ if [ -z "$mode_meta" ] && [ -d "$SESSIONS_DIR" ]; then
96
+ marker_file=""
97
+ if [ -n "$slug" ] && [ -f "$SESSIONS_DIR/$slug.mode" ]; then
98
+ marker_file="$SESSIONS_DIR/$slug.mode"
99
+ else
100
+ count="$(find "$SESSIONS_DIR" -maxdepth 1 -type f -name '*.mode' 2>/dev/null | wc -l | tr -d ' ')"
101
+ if [ "$count" = "1" ]; then
102
+ marker_file="$(find "$SESSIONS_DIR" -maxdepth 1 -type f -name '*.mode' 2>/dev/null | head -n1)"
103
+ fi
104
+ fi
105
+ if [ -n "$marker_file" ] && [ -f "$marker_file" ]; then
106
+ mode_marker="$(head -n1 "$marker_file" | tr -d '[:space:]')"
107
+ fi
108
+ fi
109
+ effective_mode="${mode_meta:-$mode_marker}"
110
+
111
+ if [ "$effective_mode" = "solo" ]; then
112
+ printf '{"ts":"%s","hook":"task-created","warn":"INVALID_FOR_SOLO_MODE","title":%s}\n' \
113
+ "$ts" "$(printf '%s' "$title" | jq -Rs .)" >> "$LOG_FILE"
114
+ fi
115
+
116
+ # v5 INVALID_WAVE_REFERENCE: wave metadata is required and must match the v5 shape.
117
+ if [ -z "$wave" ]; then
118
+ printf '{"ts":"%s","hook":"task-created","warn":"INVALID_WAVE_REFERENCE","title":%s,"reason":"wave metadata missing"}\n' \
119
+ "$ts" "$(printf '%s' "$title" | jq -Rs .)" >> "$LOG_FILE"
120
+ elif ! printf '%s' "$wave" | grep -qE '^([0-9]+\.[0-9]+|[0-9]+\.rework|qc-rework)$'; then
121
+ printf '{"ts":"%s","hook":"task-created","warn":"INVALID_WAVE_REFERENCE","title":%s,"wave":%s,"reason":"unrecognised wave shape"}\n' \
122
+ "$ts" \
123
+ "$(printf '%s' "$title" | jq -Rs .)" \
124
+ "$(printf '%s' "$wave" | jq -Rs .)" \
125
+ >> "$LOG_FILE"
126
+ fi
127
+
80
128
  # At this point title starts with `impl:`. Strip prefix and require a known
81
- # sub-prefix.
129
+ # v5 sub-prefix.
82
130
  rest="${title#impl:}"
83
131
  case "$rest" in
84
- be-*|qa-fix-be-*|review-fix-be-*|be-migration-*|be-contract-publish-*)
132
+ rework-*)
133
+ # Rework tasks dispatched by team-leader (phase-end review) or qc-engineer
134
+ # (end-of-plan QC). The Reworks: <orig-id> line in the commit body is
135
+ # validated by task-completed.sh (MISSING_REWORK_REFERENCE). No shape check
136
+ # — rework inherits the originating task's scope.
137
+ sub="rework"
138
+ ;;
139
+ be-*|be-migration-*|be-contract-publish-*)
85
140
  sub="be"
86
141
  ;;
87
- fe-*|qa-fix-fe-*|review-fix-fe-*)
142
+ fe-*)
88
143
  sub="fe"
89
144
  ;;
90
145
  contract-update-*)
@@ -92,10 +147,13 @@ case "$rest" in
92
147
  ;;
93
148
  *)
94
149
  printf '{"ts":"%s","hook":"task-created","warn":"bad_subprefix","title":%s}\n' "$ts" "$(printf '%s' "$title" | jq -Rs .)" >> "$LOG_FILE"
95
- exit 0 ;;
150
+ exit 0
151
+ ;;
96
152
  esac
97
153
 
98
- # Shape-aware enforcement
154
+ # Shape-aware enforcement. rework + contract-update are scope-neutral and
155
+ # allowed under any shape — they inherit scope from the originating task or
156
+ # from BE ownership of the contract.
99
157
  case "$shape" in
100
158
  be-only)
101
159
  if [ "$sub" = "fe" ]; then
@@ -1,15 +1,37 @@
1
1
  #!/usr/bin/env bash
2
- # TeammateIdle hook — block idle if there are unanswered inbound peer messages.
2
+ # TeammateIdle hook — v5 role-aware routing.
3
3
  #
4
- # Reads the hook event payload from stdin. Expected JSON fields (best-effort —
5
- # the agent-teams runtime may evolve; we only fail closed on what we can verify):
6
- # - mailbox: array of { from, replied, ... }
7
- # - teammate: string (the idling teammate's role)
4
+ # Reads the hook event payload from stdin. Expected JSON fields:
5
+ # - mailbox: array of { from, kind, replied, ... } recent SendMessage history
6
+ # - teammate: string (the idling teammate's role; one of
7
+ # orchestrator | team-leader | solution-architect | feature-planner |
8
+ # security-engineer | backend-developer | frontend-developer | qc-engineer)
8
9
  #
9
- # Behaviour:
10
- # - count messages where from != "lead" AND replied == false
11
- # - if count > 0 -> exit 2 with stderr BLOCKED_IDLE
12
- # - else -> exit 0
10
+ # v5 behaviour: routing depends on role.
11
+ #
12
+ # backend-developer / frontend-developer:
13
+ # - block if any unanswered message FROM team-leader (implementer owes a reply)
14
+ # - block if a self-sent ESCALATE has no reply within heartbeat (advisory log)
15
+ # - otherwise advisory: wave dispatch may still be in flight; ok to idle
16
+ #
17
+ # team-leader:
18
+ # - block if SPAWN_REQUEST or RESTART_REQUEST has no SPAWN_DONE / RESTART_DONE
19
+ # reply from lead/orchestrator within heartbeat (must keep coordinating)
20
+ # - block if any unanswered ESCALATE from an implementer (must route)
21
+ # - otherwise advisory tick
22
+ #
23
+ # qc-engineer:
24
+ # - block if QC_REWORK_NEEDED has no reply (lead must acknowledge before shutdown)
25
+ # - otherwise advisory tick
26
+ #
27
+ # solution-architect / feature-planner / security-engineer (phase A roles):
28
+ # - block if owner sign-off touchpoint message has no reply yet
29
+ # - otherwise advisory tick (shutdown is owner-driven via lead)
30
+ #
31
+ # orchestrator (lead):
32
+ # - block if any unanswered SPAWN_REQUEST / RESTART_REQUEST inbound from
33
+ # team-leader (lead is the single spawner)
34
+ # - otherwise advisory tick (lead drives owner touchpoints separately)
13
35
  #
14
36
  # Logs every invocation to .claude/hooks/log.jsonl in the project root for tuning.
15
37
 
@@ -33,12 +55,95 @@ if ! command -v jq >/dev/null 2>&1; then
33
55
  exit 0
34
56
  fi
35
57
 
36
- unanswered="$(printf '%s' "$payload" | jq '[.mailbox[]? | select((.from // "") != "lead") | select((.replied // false) == false)] | length' 2>/dev/null || echo 0)"
58
+ teammate="$(printf '%s' "$payload" | jq -r '.teammate // ""' 2>/dev/null || echo "")"
59
+
60
+ # Helper: count unanswered inbound messages where .from matches a pattern.
61
+ count_unanswered_from() {
62
+ local from_pattern="$1"
63
+ printf '%s' "$payload" \
64
+ | jq --arg p "$from_pattern" \
65
+ '[.mailbox[]?
66
+ | select(((.from // "") | test($p)))
67
+ | select((.replied // false) == false)
68
+ ] | length' \
69
+ 2>/dev/null \
70
+ || echo 0
71
+ }
72
+
73
+ # Helper: count self-sent outbound messages of a given kind with no reply yet.
74
+ count_outbound_unanswered_kind() {
75
+ local kind_pattern="$1"
76
+ printf '%s' "$payload" \
77
+ | jq --arg k "$kind_pattern" \
78
+ '[.mailbox[]?
79
+ | select(((.direction // "in") == "out"))
80
+ | select(((.kind // "") | test($k)))
81
+ | select((.replied // false) == false)
82
+ ] | length' \
83
+ 2>/dev/null \
84
+ || echo 0
85
+ }
86
+
87
+ warn=""
37
88
 
38
- printf '{"ts":"%s","hook":"teammate-idle","unanswered":%s}\n' "$ts" "$unanswered" >> "$LOG_FILE"
89
+ case "$teammate" in
90
+ backend-developer|frontend-developer)
91
+ unanswered_from_leader="$(count_unanswered_from '^team-leader$')"
92
+ if [ "${unanswered_from_leader:-0}" -gt 0 ]; then
93
+ warn="BLOCKED_IDLE_implementer_owes_team-leader_reply"
94
+ fi
95
+ ;;
96
+ team-leader)
97
+ spawn_pending="$(count_outbound_unanswered_kind 'SPAWN_REQUEST|RESTART_REQUEST')"
98
+ escalate_pending="$(count_unanswered_from '^(backend-developer|frontend-developer)$')"
99
+ if [ "${spawn_pending:-0}" -gt 0 ]; then
100
+ warn="BLOCKED_IDLE_team-leader_awaiting_lead_on_spawn_or_restart"
101
+ elif [ "${escalate_pending:-0}" -gt 0 ]; then
102
+ warn="BLOCKED_IDLE_team-leader_owes_implementer_escalate_reply"
103
+ fi
104
+ ;;
105
+ qc-engineer)
106
+ qc_pending="$(count_outbound_unanswered_kind 'QC_REWORK_NEEDED')"
107
+ if [ "${qc_pending:-0}" -gt 0 ]; then
108
+ warn="BLOCKED_IDLE_qc-engineer_awaiting_lead_ack"
109
+ fi
110
+ ;;
111
+ solution-architect|feature-planner|security-engineer)
112
+ handover_pending="$(count_outbound_unanswered_kind 'HANDOVER_READY|SEC_PASSED|SEC_BLOCKED')"
113
+ if [ "${handover_pending:-0}" -gt 0 ]; then
114
+ warn="BLOCKED_IDLE_phaseA_awaiting_owner_signoff"
115
+ fi
116
+ ;;
117
+ orchestrator)
118
+ lead_inbound_pending="$(count_unanswered_from '^team-leader$')"
119
+ if [ "${lead_inbound_pending:-0}" -gt 0 ]; then
120
+ warn="BLOCKED_IDLE_orchestrator_unhandled_team-leader_request"
121
+ fi
122
+ ;;
123
+ "")
124
+ # Unknown role: fall back to legacy v4 behaviour (any unanswered non-lead inbound).
125
+ legacy_unanswered="$(printf '%s' "$payload" | jq '[.mailbox[]? | select((.from // "") != "lead") | select((.replied // false) == false)] | length' 2>/dev/null || echo 0)"
126
+ if [ "${legacy_unanswered:-0}" -gt 0 ]; then
127
+ warn="BLOCKED_IDLE_legacy_unanswered_inbound"
128
+ fi
129
+ ;;
130
+ *)
131
+ # Unknown but non-empty role — log advisory only, do not block.
132
+ :
133
+ ;;
134
+ esac
39
135
 
40
- if [ "${unanswered:-0}" -gt 0 ]; then
41
- printf '{"ts":"%s","hook":"teammate-idle","warn":"blocked_idle","unanswered":%s}\n' "$ts" "$unanswered" >> "$LOG_FILE"
136
+ if [ -n "$warn" ]; then
137
+ printf '{"ts":"%s","hook":"teammate-idle","teammate":%s,"warn":%s}\n' \
138
+ "$ts" \
139
+ "$(printf '%s' "$teammate" | jq -Rs .)" \
140
+ "$(printf '%s' "$warn" | jq -Rs .)" \
141
+ >> "$LOG_FILE"
142
+ else
143
+ printf '{"ts":"%s","hook":"teammate-idle","teammate":%s,"ok":true}\n' \
144
+ "$ts" \
145
+ "$(printf '%s' "$teammate" | jq -Rs .)" \
146
+ >> "$LOG_FILE"
42
147
  fi
43
148
 
44
149
  exit 0