@intentsolutionsio/contributing-clanker 0.1.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.
Files changed (69) hide show
  1. package/README.md +173 -0
  2. package/hooks/.gitkeep +0 -0
  3. package/hooks/install.sh +115 -0
  4. package/hooks/uninstall.sh +70 -0
  5. package/package.json +42 -0
  6. package/skills/contribute/SKILL.md +457 -0
  7. package/skills/contribute/agents/draft-writer.md +110 -0
  8. package/skills/contribute/agents/repo-analyzer.md +68 -0
  9. package/skills/contribute/agents/researcher.md +246 -0
  10. package/skills/contribute/agents/scout.md +182 -0
  11. package/skills/contribute/agents/test-runner.md +70 -0
  12. package/skills/contribute/assets/claim-template.md +22 -0
  13. package/skills/contribute/assets/evidence-template.md +32 -0
  14. package/skills/contribute/assets/pr-template.md +49 -0
  15. package/skills/contribute/references/candidate-file-format.md +259 -0
  16. package/skills/contribute/references/workflow-guide.md +153 -0
  17. package/skills/contribute/scripts/audit-overrides.sh +180 -0
  18. package/skills/contribute/scripts/catalog-coverage.sh +99 -0
  19. package/skills/contribute/scripts/gate-runner.sh +191 -0
  20. package/skills/contribute/scripts/gates/a01-already-assigned.sh +24 -0
  21. package/skills/contribute/scripts/gates/a02-already-shipped.sh +31 -0
  22. package/skills/contribute/scripts/gates/a03-duplicate-flagged.sh +22 -0
  23. package/skills/contribute/scripts/gates/a04-issue-age.sh +80 -0
  24. package/skills/contribute/scripts/gates/a05-issue-still-open.sh +33 -0
  25. package/skills/contribute/scripts/gates/a06-claim-etiquette-required.sh +36 -0
  26. package/skills/contribute/scripts/gates/a09-mention-routing.sh +63 -0
  27. package/skills/contribute/scripts/gates/b01-base-branch.sh +29 -0
  28. package/skills/contribute/scripts/gates/b02-branch-naming.sh +34 -0
  29. package/skills/contribute/scripts/gates/b03-clone-fresh.sh +33 -0
  30. package/skills/contribute/scripts/gates/b05-dco-signoff.sh +50 -0
  31. package/skills/contribute/scripts/gates/b06-commit-format.sh +44 -0
  32. package/skills/contribute/scripts/gates/b07-scope-files.sh +68 -0
  33. package/skills/contribute/scripts/gates/b12-new-deps.sh +40 -0
  34. package/skills/contribute/scripts/gates/b14-local-checks.sh +55 -0
  35. package/skills/contribute/scripts/gates/b16-local-check-allowlist.sh +32 -0
  36. package/skills/contribute/scripts/gates/c01-draft-first.sh +23 -0
  37. package/skills/contribute/scripts/gates/c02-pr-title-format.sh +36 -0
  38. package/skills/contribute/scripts/gates/c03-pr-body-sections.sh +58 -0
  39. package/skills/contribute/scripts/gates/c04-ui-screenshots.sh +38 -0
  40. package/skills/contribute/scripts/gates/c05-test-evidence.sh +31 -0
  41. package/skills/contribute/scripts/gates/c07-coauthor-banned.sh +30 -0
  42. package/skills/contribute/scripts/gates/c09-issue-link.sh +31 -0
  43. package/skills/contribute/scripts/gates/c11-no-force-push.sh +32 -0
  44. package/skills/contribute/scripts/gates/c12-ci-green.sh +29 -0
  45. package/skills/contribute/scripts/gates/c13-bots-passed.sh +62 -0
  46. package/skills/contribute/scripts/gates/c16-no-self-merge.sh +24 -0
  47. package/skills/contribute/scripts/gates/c19-body-claim-vs-diff.sh +64 -0
  48. package/skills/contribute/scripts/gates/d02-no-ai-bug-reports.sh +48 -0
  49. package/skills/contribute/scripts/gates/d03-no-ai-pr-reviews.sh +42 -0
  50. package/skills/contribute/scripts/gates/d05-no-reopen.sh +25 -0
  51. package/skills/contribute/scripts/gates/e02-ai-strike-track.sh +57 -0
  52. package/skills/contribute/scripts/gates/e04-fork-target.sh +39 -0
  53. package/skills/contribute/scripts/gates/f01-license-compat.sh +92 -0
  54. package/skills/contribute/scripts/gates/f03-fixtures-clean.sh +30 -0
  55. package/skills/contribute/scripts/gates/f04-override-disclosure.sh +49 -0
  56. package/skills/contribute/scripts/gates/g01-no-vendored-edits.sh +52 -0
  57. package/skills/contribute/scripts/gates/g02-protected-paths.sh +30 -0
  58. package/skills/contribute/scripts/gates/g03-no-changelog-edits.sh +37 -0
  59. package/skills/contribute/scripts/gates/g04-no-version-bump.sh +36 -0
  60. package/skills/contribute/scripts/gates/g06-override-rate-limit.sh +31 -0
  61. package/skills/contribute/scripts/gates/lib/preamble.sh +105 -0
  62. package/skills/contribute/scripts/lint-candidate.sh +149 -0
  63. package/skills/contribute/scripts/researcher-build.sh +456 -0
  64. package/skills/contribute/scripts/test-known-traps.sh +142 -0
  65. package/skills/contribute/scripts/test-override-audit.sh +102 -0
  66. package/skills/contribute/scripts/test-plug-in.sh +113 -0
  67. package/skills/contribute/scripts/test-scout-refresh.sh +157 -0
  68. package/skills/contribute/scripts/test-stale-dossier-refresh.sh +96 -0
  69. package/skills/contribute/scripts/transition.sh +260 -0
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env bash
2
+ # transition.sh — invoked by /contribute SKILL.md on every lifecycle transition.
3
+ # This is the single chokepoint between "user wants to take action" and
4
+ # "external action happens." Wraps gate-runner + override resolution +
5
+ # atomic candidate update.
6
+ #
7
+ # Usage:
8
+ # transition.sh <action> <candidate-path> [options]
9
+ # action: "shortlist→claimed", "claimed→working", etc.
10
+ # options:
11
+ # --dossier <path> Override dossier path (default: derive from candidate)
12
+ # --override-gate <id> <reason> Pre-record an override before running gates (repeatable)
13
+ # --dry-run Run gates, print verdict, do NOT mutate candidate
14
+ # --max-gate-age <seconds> Reject if last gate run for this candidate is older
15
+ # (TOCTOU mitigation; default 60)
16
+ #
17
+ # Exit code: 0 if transition allowed, 1 if BLOCKed (effective after overrides).
18
+
19
+ set -euo pipefail
20
+
21
+ ACTION="${1:-}"
22
+ CANDIDATE="${2:-}"
23
+ shift 2 2>/dev/null || true
24
+
25
+ if [[ -z "$ACTION" || -z "$CANDIDATE" ]]; then
26
+ echo "usage: $0 <action> <candidate-path> [--dossier PATH] [--override-gate ID REASON ...] [--dry-run] [--max-gate-age SEC]" >&2
27
+ exit 64
28
+ fi
29
+
30
+ if [[ ! -f "$CANDIDATE" ]]; then
31
+ echo "candidate not found: $CANDIDATE" >&2
32
+ exit 65
33
+ fi
34
+
35
+ DOSSIER=""
36
+ DRY_RUN=0
37
+ MAX_GATE_AGE=60
38
+ declare -a OVERRIDES_NEW=() # pairs: gate id, reason
39
+
40
+ while [[ $# -gt 0 ]]; do
41
+ case "$1" in
42
+ --dossier)
43
+ DOSSIER="$2"; shift 2 ;;
44
+ --override-gate)
45
+ OVERRIDES_NEW+=("$2" "$3"); shift 3 ;;
46
+ --dry-run)
47
+ DRY_RUN=1; shift ;;
48
+ --max-gate-age)
49
+ # shellcheck disable=SC2034 # reserved for TOCTOU mitigation
50
+ MAX_GATE_AGE="$2"; shift 2 ;;
51
+ *)
52
+ echo "unknown option: $1" >&2; exit 64 ;;
53
+ esac
54
+ done
55
+
56
+ LOG="$HOME/.contribute-system/log.jsonl"
57
+ NOW=$(/usr/bin/date -u +%Y-%m-%dT%H:%M:%SZ)
58
+
59
+ # Derive dossier path from candidate's repo if not supplied
60
+ if [[ -z "$DOSSIER" ]]; then
61
+ REPO=$(/usr/bin/awk '/^---$/{fm=!fm?1:2;next} fm==1 && /^repo:/{sub(/^repo:[[:space:]]*/,""); print; exit}' "$CANDIDATE")
62
+ if [[ -n "$REPO" ]]; then
63
+ SLUG=$(/usr/bin/echo "$REPO" | /usr/bin/tr '/' '_')_; SLUG="${SLUG%_}" # placeholder; researcher uses double-underscore
64
+ SLUG=$(/usr/bin/echo "$REPO" | /usr/bin/sed 's,/,__,')
65
+ CAND_DOSSIER="$HOME/.contribute-system/research/${SLUG}.md"
66
+ [[ -f "$CAND_DOSSIER" ]] && DOSSIER="$CAND_DOSSIER"
67
+ fi
68
+ fi
69
+
70
+ # Pre-record any overrides into the candidate file (atomic temp+rename).
71
+ #
72
+ # Earlier implementation used `awk -v RS='---'` to insert `overrides: []`
73
+ # before the closing frontmatter delimiter, then `sed -i "/^overrides:/a"`
74
+ # to append each entry. That path corrupted YAML: the awk RS=--- handling
75
+ # mangled the opening delimiter to "------" (6 dashes) and the sed
76
+ # append landed entries OUTSIDE the array as sibling list items rather
77
+ # than children of `overrides`.
78
+ #
79
+ # Replace with a Python yaml round-trip — parse frontmatter, append to
80
+ # the overrides list as proper YAML mapping entries, re-serialize. The
81
+ # body (everything after the second `---`) passes through unchanged.
82
+ if [[ "${#OVERRIDES_NEW[@]}" -gt 0 ]]; then
83
+ if [[ "$DRY_RUN" -eq 1 ]]; then
84
+ echo "(dry-run) would record ${#OVERRIDES_NEW[@]} overrides; skipping write" >&2
85
+ else
86
+ TMP="${CANDIDATE}.tmp.$$"
87
+ # Build a flat list of "gate=reason" pairs for the Python helper.
88
+ # Use NUL as field separator to handle reasons containing any char.
89
+ PAIRS_FILE="${CANDIDATE}.pairs.$$"
90
+ : > "$PAIRS_FILE"
91
+ i=0
92
+ while [[ $i -lt ${#OVERRIDES_NEW[@]} ]]; do
93
+ /usr/bin/printf '%s\0%s\0' "${OVERRIDES_NEW[$i]}" "${OVERRIDES_NEW[$((i+1))]}" >> "$PAIRS_FILE"
94
+ i=$((i+2))
95
+ done
96
+
97
+ /usr/bin/python3 - "$CANDIDATE" "$NOW" "$PAIRS_FILE" "$TMP" <<'PYEOF'
98
+ import sys, yaml
99
+
100
+ cand_path, now_iso, pairs_path, out_path = sys.argv[1:5]
101
+
102
+ with open(cand_path) as f:
103
+ text = f.read()
104
+
105
+ # Split frontmatter from body (markdown convention: --- on its own line)
106
+ lines = text.split('\n')
107
+ if not lines or lines[0].strip() != '---':
108
+ sys.stderr.write("transition: candidate has no opening frontmatter delimiter\n")
109
+ sys.exit(2)
110
+ try:
111
+ end = lines.index('---', 1)
112
+ except ValueError:
113
+ sys.stderr.write("transition: candidate has no closing frontmatter delimiter\n")
114
+ sys.exit(2)
115
+
116
+ fm_text = '\n'.join(lines[1:end])
117
+ body_text = '\n'.join(lines[end+1:])
118
+
119
+ fm = yaml.safe_load(fm_text) or {}
120
+ if not isinstance(fm, dict):
121
+ sys.stderr.write("transition: frontmatter is not a mapping\n")
122
+ sys.exit(2)
123
+
124
+ if 'overrides' not in fm or fm['overrides'] is None:
125
+ fm['overrides'] = []
126
+ if not isinstance(fm['overrides'], list):
127
+ sys.stderr.write("transition: existing overrides field is not a list\n")
128
+ sys.exit(2)
129
+
130
+ # Read NUL-separated pairs from pairs_path: gate, reason, gate, reason, ...
131
+ with open(pairs_path, 'rb') as f:
132
+ raw = f.read()
133
+ parts = raw.split(b'\x00')
134
+ # Trailing NUL produces an empty final element — drop it.
135
+ if parts and parts[-1] == b'':
136
+ parts.pop()
137
+ if len(parts) % 2 != 0:
138
+ sys.stderr.write("transition: malformed override pairs (odd count)\n")
139
+ sys.exit(2)
140
+
141
+ for i in range(0, len(parts), 2):
142
+ gate = parts[i].decode('utf-8')
143
+ reason = parts[i+1].decode('utf-8')
144
+ fm['overrides'].append({'gate': gate, 'reason': reason, 'at': now_iso})
145
+
146
+ # Re-serialize. default_flow_style=False keeps blocks readable.
147
+ new_fm = yaml.safe_dump(fm, default_flow_style=False, sort_keys=False, allow_unicode=True).rstrip('\n')
148
+ new_text = '---\n' + new_fm + '\n---\n' + body_text
149
+
150
+ with open(out_path, 'w') as f:
151
+ f.write(new_text)
152
+ PYEOF
153
+
154
+ PY_EXIT=$?
155
+ /usr/bin/rm -f "$PAIRS_FILE"
156
+ if [[ "$PY_EXIT" -ne 0 ]]; then
157
+ /usr/bin/rm -f "$TMP"
158
+ echo "transition: yaml round-trip failed (exit $PY_EXIT) — candidate not modified" >&2
159
+ exit 65
160
+ fi
161
+
162
+ /usr/bin/mv "$TMP" "$CANDIDATE" # atomic rename
163
+
164
+ # Log each override
165
+ i=0
166
+ while [[ $i -lt ${#OVERRIDES_NEW[@]} ]]; do
167
+ OG="${OVERRIDES_NEW[$i]}"
168
+ OR="${OVERRIDES_NEW[$((i+1))]}"
169
+ i=$((i+2))
170
+ jq -nc --arg ts "$NOW" --arg gate "$OG" --arg reason "$OR" --arg cand "$CANDIDATE" \
171
+ '{ts: $ts, event: "gate_override", details: {gate: $gate, reason: $reason, candidate: $cand}}' >> "$LOG"
172
+ done
173
+ fi
174
+ fi
175
+
176
+ # Advisory: warn on missing required body sections per target status.
177
+ # Per skills/contribute/references/candidate-file-format.md § "Required
178
+ # sections by lifecycle stage". WARN (not BLOCK) — backfilled candidates
179
+ # legitimately came in mid-lifecycle without early-stage sections.
180
+ TARGET_STATUS="${ACTION##*→}"
181
+ REQUIRED_SECTIONS=""
182
+ case "$TARGET_STATUS" in
183
+ shortlist) REQUIRED_SECTIONS="## Scope|## Files to touch" ;;
184
+ claimed) REQUIRED_SECTIONS="## Scope|## Files to touch|## Claim comment draft" ;;
185
+ working) REQUIRED_SECTIONS="## Scope|## Files to touch|## Claim comment draft" ;;
186
+ submitted) REQUIRED_SECTIONS="## PR title|## PR body|## Test results" ;;
187
+ merged) REQUIRED_SECTIONS="## PR title|## PR body|## Test results" ;;
188
+ *) REQUIRED_SECTIONS="" ;; # open, dropped: no body requirements
189
+ esac
190
+
191
+ MISSING_SECTIONS=()
192
+ if [[ -n "$REQUIRED_SECTIONS" ]]; then
193
+ IFS='|' read -ra SECTIONS <<< "$REQUIRED_SECTIONS"
194
+ for sec in "${SECTIONS[@]}"; do
195
+ # Match the section header anchored at line start (avoid false positives
196
+ # inside code blocks or quoted text).
197
+ if ! /usr/bin/grep -qE "^${sec}\b" "$CANDIDATE"; then
198
+ MISSING_SECTIONS+=("$sec")
199
+ fi
200
+ done
201
+ fi
202
+
203
+ if [[ "${#MISSING_SECTIONS[@]}" -gt 0 ]]; then
204
+ /usr/bin/printf '[transition] WARN: candidate is missing %d required section(s) for status=%s:\n' \
205
+ "${#MISSING_SECTIONS[@]}" "$TARGET_STATUS" >&2
206
+ for sec in "${MISSING_SECTIONS[@]}"; do
207
+ /usr/bin/printf '[transition] - %s\n' "$sec" >&2
208
+ done
209
+ /usr/bin/printf '[transition] (advisory only — see references/candidate-file-format.md;\n' >&2
210
+ /usr/bin/printf '[transition] backfilled candidates legitimately skip early-stage sections)\n' >&2
211
+ # Log it so audits can see the pattern frequency
212
+ MISSING_JSON=$(/usr/bin/printf '%s\n' "${MISSING_SECTIONS[@]}" | jq -Rsc 'split("\n") | map(select(. != ""))')
213
+ jq -nc --arg ts "$NOW" --arg cand "$CANDIDATE" --arg target "$TARGET_STATUS" \
214
+ --argjson missing "$MISSING_JSON" \
215
+ '{ts: $ts, event: "transition_section_warn",
216
+ details: {candidate: $cand, target_status: $target, missing_sections: $missing}}' \
217
+ >> "$LOG" 2>/dev/null || true
218
+ fi
219
+
220
+ # Run gate-runner
221
+ /usr/bin/printf '\n[transition] %s on %s\n' "$ACTION" "$(/usr/bin/basename "$CANDIDATE")" >&2
222
+ [[ -n "$DOSSIER" ]] && /usr/bin/printf '[transition] dossier: %s\n' "$DOSSIER" >&2 || /usr/bin/printf '[transition] dossier: (none — gates that need it will SKIP)\n' >&2
223
+
224
+ set +e
225
+ # Find gate-runner co-located with this script (works whether invoked from
226
+ # ~/.contribute-system/bin/ or from the skill's scripts/ dir).
227
+ _TRANSITION_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
228
+ GATE_VERDICT=$("${_TRANSITION_DIR}/gate-runner.sh" "$ACTION" "$CANDIDATE" "$DOSSIER")
229
+ GATE_EXIT=$?
230
+ set -e
231
+
232
+ # Surface verdict
233
+ echo "$GATE_VERDICT"
234
+
235
+ # Log the transition attempt
236
+ jq -nc --arg ts "$NOW" --arg action "$ACTION" --arg cand "$CANDIDATE" --arg exit "$GATE_EXIT" --arg verdict "$GATE_VERDICT" \
237
+ '{ts: $ts, event: "transition_attempt", details: {action: $action, candidate: $cand, gate_exit: $exit | tonumber, gate_verdict: ($verdict | fromjson? // {raw: $verdict})}}' >> "$LOG" 2>/dev/null || true
238
+
239
+ if [[ "$GATE_EXIT" -ne 0 ]]; then
240
+ /usr/bin/printf '\n[transition] BLOCKED. Resolve the BLOCKers above or use --override-gate.\n\n' >&2
241
+ exit 1
242
+ fi
243
+
244
+ # Update candidate state if not dry-run
245
+ if [[ "$DRY_RUN" -eq 0 ]]; then
246
+ # Parse target state from action ("foo→bar" → "bar")
247
+ NEW_STATE="${ACTION##*→}"
248
+ if [[ "$NEW_STATE" != "$ACTION" && -n "$NEW_STATE" ]]; then
249
+ TMP="${CANDIDATE}.tmp.$$"
250
+ /usr/bin/sed "s/^status: .*/status: $NEW_STATE/" "$CANDIDATE" > "$TMP"
251
+ /usr/bin/mv "$TMP" "$CANDIDATE" # atomic
252
+ /usr/bin/printf '[transition] candidate status → %s\n\n' "$NEW_STATE" >&2
253
+
254
+ # Log success
255
+ jq -nc --arg ts "$NOW" --arg action "$ACTION" --arg cand "$CANDIDATE" --arg new_state "$NEW_STATE" \
256
+ '{ts: $ts, event: "transition_committed", details: {action: $action, candidate: $cand, new_state: $new_state}}' >> "$LOG" 2>/dev/null || true
257
+ fi
258
+ fi
259
+
260
+ exit 0