@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.
- package/README.md +173 -0
- package/hooks/.gitkeep +0 -0
- package/hooks/install.sh +115 -0
- package/hooks/uninstall.sh +70 -0
- package/package.json +42 -0
- package/skills/contribute/SKILL.md +457 -0
- package/skills/contribute/agents/draft-writer.md +110 -0
- package/skills/contribute/agents/repo-analyzer.md +68 -0
- package/skills/contribute/agents/researcher.md +246 -0
- package/skills/contribute/agents/scout.md +182 -0
- package/skills/contribute/agents/test-runner.md +70 -0
- package/skills/contribute/assets/claim-template.md +22 -0
- package/skills/contribute/assets/evidence-template.md +32 -0
- package/skills/contribute/assets/pr-template.md +49 -0
- package/skills/contribute/references/candidate-file-format.md +259 -0
- package/skills/contribute/references/workflow-guide.md +153 -0
- package/skills/contribute/scripts/audit-overrides.sh +180 -0
- package/skills/contribute/scripts/catalog-coverage.sh +99 -0
- package/skills/contribute/scripts/gate-runner.sh +191 -0
- package/skills/contribute/scripts/gates/a01-already-assigned.sh +24 -0
- package/skills/contribute/scripts/gates/a02-already-shipped.sh +31 -0
- package/skills/contribute/scripts/gates/a03-duplicate-flagged.sh +22 -0
- package/skills/contribute/scripts/gates/a04-issue-age.sh +80 -0
- package/skills/contribute/scripts/gates/a05-issue-still-open.sh +33 -0
- package/skills/contribute/scripts/gates/a06-claim-etiquette-required.sh +36 -0
- package/skills/contribute/scripts/gates/a09-mention-routing.sh +63 -0
- package/skills/contribute/scripts/gates/b01-base-branch.sh +29 -0
- package/skills/contribute/scripts/gates/b02-branch-naming.sh +34 -0
- package/skills/contribute/scripts/gates/b03-clone-fresh.sh +33 -0
- package/skills/contribute/scripts/gates/b05-dco-signoff.sh +50 -0
- package/skills/contribute/scripts/gates/b06-commit-format.sh +44 -0
- package/skills/contribute/scripts/gates/b07-scope-files.sh +68 -0
- package/skills/contribute/scripts/gates/b12-new-deps.sh +40 -0
- package/skills/contribute/scripts/gates/b14-local-checks.sh +55 -0
- package/skills/contribute/scripts/gates/b16-local-check-allowlist.sh +32 -0
- package/skills/contribute/scripts/gates/c01-draft-first.sh +23 -0
- package/skills/contribute/scripts/gates/c02-pr-title-format.sh +36 -0
- package/skills/contribute/scripts/gates/c03-pr-body-sections.sh +58 -0
- package/skills/contribute/scripts/gates/c04-ui-screenshots.sh +38 -0
- package/skills/contribute/scripts/gates/c05-test-evidence.sh +31 -0
- package/skills/contribute/scripts/gates/c07-coauthor-banned.sh +30 -0
- package/skills/contribute/scripts/gates/c09-issue-link.sh +31 -0
- package/skills/contribute/scripts/gates/c11-no-force-push.sh +32 -0
- package/skills/contribute/scripts/gates/c12-ci-green.sh +29 -0
- package/skills/contribute/scripts/gates/c13-bots-passed.sh +62 -0
- package/skills/contribute/scripts/gates/c16-no-self-merge.sh +24 -0
- package/skills/contribute/scripts/gates/c19-body-claim-vs-diff.sh +64 -0
- package/skills/contribute/scripts/gates/d02-no-ai-bug-reports.sh +48 -0
- package/skills/contribute/scripts/gates/d03-no-ai-pr-reviews.sh +42 -0
- package/skills/contribute/scripts/gates/d05-no-reopen.sh +25 -0
- package/skills/contribute/scripts/gates/e02-ai-strike-track.sh +57 -0
- package/skills/contribute/scripts/gates/e04-fork-target.sh +39 -0
- package/skills/contribute/scripts/gates/f01-license-compat.sh +92 -0
- package/skills/contribute/scripts/gates/f03-fixtures-clean.sh +30 -0
- package/skills/contribute/scripts/gates/f04-override-disclosure.sh +49 -0
- package/skills/contribute/scripts/gates/g01-no-vendored-edits.sh +52 -0
- package/skills/contribute/scripts/gates/g02-protected-paths.sh +30 -0
- package/skills/contribute/scripts/gates/g03-no-changelog-edits.sh +37 -0
- package/skills/contribute/scripts/gates/g04-no-version-bump.sh +36 -0
- package/skills/contribute/scripts/gates/g06-override-rate-limit.sh +31 -0
- package/skills/contribute/scripts/gates/lib/preamble.sh +105 -0
- package/skills/contribute/scripts/lint-candidate.sh +149 -0
- package/skills/contribute/scripts/researcher-build.sh +456 -0
- package/skills/contribute/scripts/test-known-traps.sh +142 -0
- package/skills/contribute/scripts/test-override-audit.sh +102 -0
- package/skills/contribute/scripts/test-plug-in.sh +113 -0
- package/skills/contribute/scripts/test-scout-refresh.sh +157 -0
- package/skills/contribute/scripts/test-stale-dossier-refresh.sh +96 -0
- 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
|