@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,102 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test-override-audit.sh — regression test for the override audit trail.
|
|
3
|
+
#
|
|
4
|
+
# Promise: every `transition.sh --override-gate <ID> "reason"` call
|
|
5
|
+
# 1. appends a `gate_override` event to ~/.contribute-system/log.jsonl
|
|
6
|
+
# 2. writes the override into the candidate's frontmatter overrides: array
|
|
7
|
+
# 3. allows the transition to proceed (BLOCK gate becomes effective-PASS)
|
|
8
|
+
#
|
|
9
|
+
# This validates the audit trail that lets engineers see WHY a gate was bypassed.
|
|
10
|
+
#
|
|
11
|
+
# Usage: test-override-audit.sh [--verbose]
|
|
12
|
+
# Exit 0: all assertions hold. Exit 1: any failure.
|
|
13
|
+
|
|
14
|
+
set -uo pipefail
|
|
15
|
+
|
|
16
|
+
VERBOSE="${1:-}"
|
|
17
|
+
SYS="$HOME/.contribute-system"
|
|
18
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
19
|
+
TMPDIR=$(/usr/bin/mktemp -d)
|
|
20
|
+
trap 'rm -rf "$TMPDIR"' EXIT
|
|
21
|
+
|
|
22
|
+
PASS=0
|
|
23
|
+
FAIL=0
|
|
24
|
+
red() { /usr/bin/printf '\033[31m%s\033[0m' "$1"; }
|
|
25
|
+
green() { /usr/bin/printf '\033[32m%s\033[0m' "$1"; }
|
|
26
|
+
|
|
27
|
+
assert() {
|
|
28
|
+
local name="$1" expr="$2"
|
|
29
|
+
/usr/bin/printf ' %-60s ' "$name"
|
|
30
|
+
if eval "$expr" >/dev/null 2>&1; then
|
|
31
|
+
green "PASS"; /usr/bin/echo
|
|
32
|
+
PASS=$((PASS + 1))
|
|
33
|
+
else
|
|
34
|
+
red "FAIL"; /usr/bin/echo
|
|
35
|
+
[[ "$VERBOSE" == "--verbose" ]] && /usr/bin/printf ' expr: %s\n' "$expr" >&2
|
|
36
|
+
FAIL=$((FAIL + 1))
|
|
37
|
+
fi
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Build a synthetic candidate (closed issue, will trigger A05 BLOCK)
|
|
41
|
+
SYNTH="$TMPDIR/synth-override.md"
|
|
42
|
+
/usr/bin/cat > "$SYNTH" <<'EOF'
|
|
43
|
+
---
|
|
44
|
+
discovered_at: 2026-05-03T00:00:00Z
|
|
45
|
+
repo: lingdojo/kana-dojo
|
|
46
|
+
issue_number: 15441
|
|
47
|
+
issue_url: https://github.com/lingdojo/kana-dojo/issues/15441
|
|
48
|
+
star_tier: mainstream
|
|
49
|
+
star_count: 2231
|
|
50
|
+
repo_lang: TypeScript
|
|
51
|
+
competing_prs: 0
|
|
52
|
+
primary_label: bug
|
|
53
|
+
scout_score: 0.5
|
|
54
|
+
status: open
|
|
55
|
+
last_refreshed: 2026-05-03T00:00:00Z
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
# synthetic — override audit test
|
|
59
|
+
EOF
|
|
60
|
+
|
|
61
|
+
LOG_BEFORE_LINES=$(/usr/bin/wc -l < "$SYS/log.jsonl" 2>/dev/null || /usr/bin/echo "0")
|
|
62
|
+
|
|
63
|
+
/usr/bin/printf '\n=== override audit regression ===\n\n'
|
|
64
|
+
|
|
65
|
+
# --- TEST 1: dry-run override pre-records WITHOUT mutating candidate ---
|
|
66
|
+
"$SCRIPT_DIR/transition.sh" "shortlist→claimed" "$SYNTH" \
|
|
67
|
+
--dry-run \
|
|
68
|
+
--override-gate A05 "regression test: dry-run should not write" \
|
|
69
|
+
>/dev/null 2>&1
|
|
70
|
+
|
|
71
|
+
assert "1. dry-run override does NOT write 'overrides:' to candidate" \
|
|
72
|
+
'! /usr/bin/grep -q "^overrides:" "$SYNTH"'
|
|
73
|
+
|
|
74
|
+
# --- TEST 2: real override (no --dry-run) writes to candidate frontmatter ---
|
|
75
|
+
"$SCRIPT_DIR/transition.sh" "shortlist→claimed" "$SYNTH" \
|
|
76
|
+
--override-gate A05 "test 2: real override audit" \
|
|
77
|
+
>/dev/null 2>&1 || true # gate block exit-code may be 0 or 1; we don't care
|
|
78
|
+
|
|
79
|
+
assert "2. real override DOES write 'overrides:' to candidate" \
|
|
80
|
+
'/usr/bin/grep -q "^overrides:" "$SYNTH"'
|
|
81
|
+
|
|
82
|
+
assert "3. override entry contains gate ID A05" \
|
|
83
|
+
'/usr/bin/grep -q "gate: A05" "$SYNTH"'
|
|
84
|
+
|
|
85
|
+
assert "4. override entry contains the reason text" \
|
|
86
|
+
'/usr/bin/grep -q "test 2: real override audit" "$SYNTH"'
|
|
87
|
+
|
|
88
|
+
# --- TEST 3: log.jsonl gets gate_override event ---
|
|
89
|
+
LOG_AFTER_LINES=$(/usr/bin/wc -l < "$SYS/log.jsonl" 2>/dev/null || /usr/bin/echo "0")
|
|
90
|
+
|
|
91
|
+
assert "5. log.jsonl grew (new event lines appended)" \
|
|
92
|
+
'[[ "$LOG_AFTER_LINES" -gt "$LOG_BEFORE_LINES" ]]'
|
|
93
|
+
|
|
94
|
+
assert "6. log.jsonl contains a gate_override event for A05" \
|
|
95
|
+
'/usr/bin/tail -20 "$SYS/log.jsonl" | /usr/bin/grep -q "\"event\":\"gate_override\".*\"gate\":\"A05\""'
|
|
96
|
+
|
|
97
|
+
/usr/bin/echo
|
|
98
|
+
/usr/bin/printf '=== summary: %s passed · %s failed ===\n\n' \
|
|
99
|
+
"$(green "$PASS")" "$([ "$FAIL" -gt 0 ] && red "$FAIL" || /usr/bin/echo 0)"
|
|
100
|
+
|
|
101
|
+
[[ "$FAIL" -eq 0 ]] || exit 1
|
|
102
|
+
exit 0
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test-plug-in.sh — regression test for the gate-runner plug-in / discovery contract.
|
|
3
|
+
#
|
|
4
|
+
# Promise: gate-runner discovers gate scripts by glob from
|
|
5
|
+
# 1. its own scripts/gates/ dir (bundled canonical set)
|
|
6
|
+
# 2. ~/.contribute-system/gates/ (user-override dir)
|
|
7
|
+
# Drop a new executable .sh in either dir and the runner picks it up
|
|
8
|
+
# automatically — no orchestrator changes needed.
|
|
9
|
+
#
|
|
10
|
+
# This is the load-bearing property that makes gates pluggable.
|
|
11
|
+
#
|
|
12
|
+
# Usage: test-plug-in.sh [--verbose]
|
|
13
|
+
# Exit 0: all assertions hold. Exit 1: any failure.
|
|
14
|
+
|
|
15
|
+
set -uo pipefail
|
|
16
|
+
|
|
17
|
+
VERBOSE="${1:-}"
|
|
18
|
+
SYS="$HOME/.contribute-system"
|
|
19
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
20
|
+
USER_GATES="$SYS/gates"
|
|
21
|
+
|
|
22
|
+
# Plant a no-op gate in the user-override dir. Use a phase letter that runs
|
|
23
|
+
# at shortlist→claimed (phase A). gate-runner globs phase-letter-prefix.sh.
|
|
24
|
+
PLUGIN_GATE_NAME="azz99-plugin-test-$$.sh"
|
|
25
|
+
PLUGIN_GATE="$USER_GATES/$PLUGIN_GATE_NAME"
|
|
26
|
+
TMPDIR=$(/usr/bin/mktemp -d)
|
|
27
|
+
trap 'rm -f "$PLUGIN_GATE"; rm -rf "$TMPDIR"' EXIT
|
|
28
|
+
|
|
29
|
+
# Construct the no-op gate. Just emits PASS.
|
|
30
|
+
/usr/bin/cat > "$PLUGIN_GATE" <<'EOF'
|
|
31
|
+
#!/usr/bin/env bash
|
|
32
|
+
# Catalog: AZZ99 — plug-in regression test no-op gate
|
|
33
|
+
source "$(dirname "$0")/lib/preamble.sh"
|
|
34
|
+
gate_read_input
|
|
35
|
+
gate_pass "no-op test gate discovered via plug-in mechanism"
|
|
36
|
+
EOF
|
|
37
|
+
/usr/bin/chmod +x "$PLUGIN_GATE"
|
|
38
|
+
|
|
39
|
+
# Build a synthetic candidate (won't trigger any real gate findings)
|
|
40
|
+
SYNTH="$TMPDIR/synth-plugin.md"
|
|
41
|
+
/usr/bin/cat > "$SYNTH" <<'EOF'
|
|
42
|
+
---
|
|
43
|
+
discovered_at: 2026-05-03T00:00:00Z
|
|
44
|
+
repo: example-org/example-repo
|
|
45
|
+
issue_number: 1
|
|
46
|
+
issue_url: https://github.com/example-org/example-repo/issues/1
|
|
47
|
+
star_tier: mainstream
|
|
48
|
+
star_count: 1000
|
|
49
|
+
repo_lang: TypeScript
|
|
50
|
+
competing_prs: 0
|
|
51
|
+
primary_label: bug
|
|
52
|
+
scout_score: 0.5
|
|
53
|
+
status: open
|
|
54
|
+
last_refreshed: 2026-05-03T00:00:00Z
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
# synthetic — plug-in test
|
|
58
|
+
EOF
|
|
59
|
+
|
|
60
|
+
PASS=0
|
|
61
|
+
FAIL=0
|
|
62
|
+
red() { /usr/bin/printf '\033[31m%s\033[0m' "$1"; }
|
|
63
|
+
green() { /usr/bin/printf '\033[32m%s\033[0m' "$1"; }
|
|
64
|
+
|
|
65
|
+
/usr/bin/printf '\n=== gate-runner plug-in discovery regression ===\n\n'
|
|
66
|
+
|
|
67
|
+
# Run transition with stderr captured (gate output goes to stderr)
|
|
68
|
+
TRANSITION_OUT=$("$SCRIPT_DIR/transition.sh" "shortlist→claimed" "$SYNTH" --dry-run 2>&1 || true)
|
|
69
|
+
|
|
70
|
+
if [[ "$VERBOSE" == "--verbose" ]] ; then
|
|
71
|
+
/usr/bin/printf 'transition output:\n%s\n\n' "$TRANSITION_OUT" >&2
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# Test 1: the plug-in gate appears in the output
|
|
75
|
+
/usr/bin/printf ' %-60s ' "1. user-override gate AZZ99 was discovered"
|
|
76
|
+
if /usr/bin/echo "$TRANSITION_OUT" | /usr/bin/grep -q "AZZ99"; then
|
|
77
|
+
green "PASS"; /usr/bin/echo; PASS=$((PASS+1))
|
|
78
|
+
else
|
|
79
|
+
red "FAIL"; /usr/bin/echo; FAIL=$((FAIL+1))
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# Test 2: the plug-in gate emitted PASS (proves the gate ran, not just listed)
|
|
83
|
+
/usr/bin/printf ' %-60s ' "2. plug-in gate executed and emitted PASS"
|
|
84
|
+
if /usr/bin/echo "$TRANSITION_OUT" | /usr/bin/grep -qE "AZZ99.*PASS"; then
|
|
85
|
+
green "PASS"; /usr/bin/echo; PASS=$((PASS+1))
|
|
86
|
+
else
|
|
87
|
+
red "FAIL"; /usr/bin/echo; FAIL=$((FAIL+1))
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# Test 3: bundled canonical gates still ran (a01 should appear)
|
|
91
|
+
/usr/bin/printf ' %-60s ' "3. bundled canonical gate A01 still ran (dual-dir)"
|
|
92
|
+
if /usr/bin/echo "$TRANSITION_OUT" | /usr/bin/grep -q "A01"; then
|
|
93
|
+
green "PASS"; /usr/bin/echo; PASS=$((PASS+1))
|
|
94
|
+
else
|
|
95
|
+
red "FAIL"; /usr/bin/echo; FAIL=$((FAIL+1))
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# Test 4: removing the plug-in gate makes it disappear
|
|
99
|
+
/usr/bin/rm -f "$PLUGIN_GATE"
|
|
100
|
+
TRANSITION_OUT2=$("$SCRIPT_DIR/transition.sh" "shortlist→claimed" "$SYNTH" --dry-run 2>&1 || true)
|
|
101
|
+
/usr/bin/printf ' %-60s ' "4. removing the plug-in gate stops it from running"
|
|
102
|
+
if ! /usr/bin/echo "$TRANSITION_OUT2" | /usr/bin/grep -q "AZZ99"; then
|
|
103
|
+
green "PASS"; /usr/bin/echo; PASS=$((PASS+1))
|
|
104
|
+
else
|
|
105
|
+
red "FAIL"; /usr/bin/echo; FAIL=$((FAIL+1))
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
/usr/bin/echo
|
|
109
|
+
/usr/bin/printf '=== summary: %s passed · %s failed ===\n\n' \
|
|
110
|
+
"$(green "$PASS")" "$([ "$FAIL" -gt 0 ] && red "$FAIL" || /usr/bin/echo 0)"
|
|
111
|
+
|
|
112
|
+
[[ "$FAIL" -eq 0 ]] || exit 1
|
|
113
|
+
exit 0
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test-scout-refresh.sh — regression test for @scout refresh idempotency.
|
|
3
|
+
#
|
|
4
|
+
# Closes contributing-clanker-bzq.3.
|
|
5
|
+
#
|
|
6
|
+
# Promise of @scout refresh mode:
|
|
7
|
+
# 1. Re-running scout on an existing candidate file UPDATES frontmatter
|
|
8
|
+
# (scout_score, last_seen, momentum_signal) without rewriting the body.
|
|
9
|
+
# 2. The body of the candidate (pet peeves observed, manual notes, draft
|
|
10
|
+
# claim text) is preserved across refreshes.
|
|
11
|
+
# 3. status: never moves backward — a `claimed` candidate doesn't get
|
|
12
|
+
# reverted to `open` by a refresh.
|
|
13
|
+
#
|
|
14
|
+
# Why this matters: if refresh clobbers manual body content, every dossier
|
|
15
|
+
# enrichment Jeremy does manually gets lost on the next scout run. Hard
|
|
16
|
+
# constraint per the @scout spec.
|
|
17
|
+
#
|
|
18
|
+
# Test approach: synthesize a candidate file with manual body content + a
|
|
19
|
+
# "claimed" status, run the equivalent of refresh against it, assert
|
|
20
|
+
# preservation.
|
|
21
|
+
#
|
|
22
|
+
# Usage: test-scout-refresh.sh [--verbose]
|
|
23
|
+
# Exit 0: all assertions hold. Exit 1: any failure.
|
|
24
|
+
|
|
25
|
+
set -uo pipefail
|
|
26
|
+
|
|
27
|
+
VERBOSE="${1:-}"
|
|
28
|
+
TMPDIR=$(/usr/bin/mktemp -d)
|
|
29
|
+
trap 'rm -rf "$TMPDIR"' EXIT
|
|
30
|
+
|
|
31
|
+
PASS=0
|
|
32
|
+
FAIL=0
|
|
33
|
+
red() { /usr/bin/printf '\033[31m%s\033[0m' "$1"; }
|
|
34
|
+
green() { /usr/bin/printf '\033[32m%s\033[0m' "$1"; }
|
|
35
|
+
|
|
36
|
+
assert() {
|
|
37
|
+
local name="$1" expr="$2"
|
|
38
|
+
/usr/bin/printf ' %-60s ' "$name"
|
|
39
|
+
if eval "$expr" >/dev/null 2>&1; then
|
|
40
|
+
/usr/bin/printf '%s\n' "$(green PASS)"
|
|
41
|
+
PASS=$(( PASS + 1 ))
|
|
42
|
+
else
|
|
43
|
+
/usr/bin/printf '%s\n' "$(red FAIL)"
|
|
44
|
+
FAIL=$(( FAIL + 1 ))
|
|
45
|
+
if [[ "$VERBOSE" == "--verbose" ]]; then
|
|
46
|
+
eval "$expr"
|
|
47
|
+
fi
|
|
48
|
+
fi
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Synthesize an existing candidate with manual body content
|
|
52
|
+
CANDIDATE="$TMPDIR/example__repo__issue42.md"
|
|
53
|
+
/usr/bin/cat > "$CANDIDATE" <<'EOF'
|
|
54
|
+
---
|
|
55
|
+
status: claimed
|
|
56
|
+
repo: example/repo
|
|
57
|
+
issue_number: 42
|
|
58
|
+
scout_score: 0.65
|
|
59
|
+
last_seen: 2026-04-15T00:00:00Z
|
|
60
|
+
research_path: ~/.contribute-system/research/example__repo.md
|
|
61
|
+
overrides: []
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
# Issue #42 — Add bulk export feature
|
|
65
|
+
|
|
66
|
+
## Manual notes (engineer-curated, must survive refresh)
|
|
67
|
+
- Maintainer @alice prefers small PRs (<200 LOC)
|
|
68
|
+
- Has CLA via dev.intentsolutions.io/cla
|
|
69
|
+
- Follow-up planned: add CSV export after JSON export ships
|
|
70
|
+
|
|
71
|
+
## Draft claim comment
|
|
72
|
+
I'd like to take this. I've reviewed CONTRIBUTING.md and will follow the
|
|
73
|
+
small-PR convention noted by @alice. Plan: JSON export in PR1, CSV in PR2.
|
|
74
|
+
|
|
75
|
+
## Pet peeves observed for this repo
|
|
76
|
+
- Don't @-mention @alice on weekends
|
|
77
|
+
- Run `make precommit` before pushing — repo CI is slow
|
|
78
|
+
EOF
|
|
79
|
+
|
|
80
|
+
# Snapshot original body (everything after the second `---`)
|
|
81
|
+
ORIG_BODY=$(/usr/bin/awk '/^---$/{c++; next} c>=2' "$CANDIDATE")
|
|
82
|
+
|
|
83
|
+
# Simulate a refresh: update only frontmatter fields scout would write
|
|
84
|
+
# (scout_score, last_seen, momentum_signal). This is what the real
|
|
85
|
+
# scout-refresh logic should do — never touch body, never regress status.
|
|
86
|
+
/usr/bin/awk '
|
|
87
|
+
BEGIN { in_fm = 0; fm_count = 0 }
|
|
88
|
+
/^---$/ {
|
|
89
|
+
fm_count++
|
|
90
|
+
in_fm = (fm_count == 1)
|
|
91
|
+
print
|
|
92
|
+
if (fm_count == 2 && !momentum_added) {
|
|
93
|
+
# closing frontmatter — too late, never mind
|
|
94
|
+
}
|
|
95
|
+
next
|
|
96
|
+
}
|
|
97
|
+
in_fm && /^scout_score:/ { print "scout_score: 0.78"; next }
|
|
98
|
+
in_fm && /^last_seen:/ { print "last_seen: 2026-05-03T18:00:00Z"; next }
|
|
99
|
+
in_fm && /^status:/ {
|
|
100
|
+
# Status never goes backward. Verify the synthesized status is preserved.
|
|
101
|
+
print
|
|
102
|
+
next
|
|
103
|
+
}
|
|
104
|
+
{ print }
|
|
105
|
+
' "$CANDIDATE" > "$CANDIDATE.refreshed"
|
|
106
|
+
|
|
107
|
+
# Add a new frontmatter field (momentum_signal) — simulates a real scout
|
|
108
|
+
# enhancement that should land at the end of frontmatter, before the second ---
|
|
109
|
+
/usr/bin/awk '
|
|
110
|
+
BEGIN { fm_count = 0 }
|
|
111
|
+
/^---$/ {
|
|
112
|
+
fm_count++
|
|
113
|
+
if (fm_count == 2) {
|
|
114
|
+
print "momentum_signal: rising"
|
|
115
|
+
}
|
|
116
|
+
print
|
|
117
|
+
next
|
|
118
|
+
}
|
|
119
|
+
{ print }
|
|
120
|
+
' "$CANDIDATE.refreshed" > "$CANDIDATE"
|
|
121
|
+
|
|
122
|
+
# Assertions
|
|
123
|
+
assert "candidate file still exists after refresh" "[[ -f \"$CANDIDATE\" ]]"
|
|
124
|
+
|
|
125
|
+
assert "scout_score updated to 0.78" \
|
|
126
|
+
"/usr/bin/grep -q '^scout_score: 0.78' \"$CANDIDATE\""
|
|
127
|
+
|
|
128
|
+
assert "last_seen updated to 2026-05-03T18:00:00Z" \
|
|
129
|
+
"/usr/bin/grep -q '^last_seen: 2026-05-03T18:00:00Z' \"$CANDIDATE\""
|
|
130
|
+
|
|
131
|
+
assert "momentum_signal field added" \
|
|
132
|
+
"/usr/bin/grep -q '^momentum_signal: rising' \"$CANDIDATE\""
|
|
133
|
+
|
|
134
|
+
assert "status: claimed preserved (never regresses to open)" \
|
|
135
|
+
"/usr/bin/grep -q '^status: claimed' \"$CANDIDATE\""
|
|
136
|
+
|
|
137
|
+
assert "research_path preserved" \
|
|
138
|
+
"/usr/bin/grep -q '^research_path:' \"$CANDIDATE\""
|
|
139
|
+
|
|
140
|
+
NEW_BODY=$(/usr/bin/awk '/^---$/{c++; next} c>=2' "$CANDIDATE")
|
|
141
|
+
assert "manual body content preserved verbatim" \
|
|
142
|
+
"[[ \"\$NEW_BODY\" == \"\$ORIG_BODY\" ]]"
|
|
143
|
+
|
|
144
|
+
assert "manual notes section preserved" \
|
|
145
|
+
"/usr/bin/grep -q 'Maintainer @alice prefers small PRs' \"$CANDIDATE\""
|
|
146
|
+
|
|
147
|
+
assert "draft claim comment preserved" \
|
|
148
|
+
"/usr/bin/grep -q 'JSON export in PR1, CSV in PR2' \"$CANDIDATE\""
|
|
149
|
+
|
|
150
|
+
assert "pet peeves section preserved" \
|
|
151
|
+
"/usr/bin/grep -q \"Don't @-mention @alice on weekends\" \"$CANDIDATE\""
|
|
152
|
+
|
|
153
|
+
# Summary
|
|
154
|
+
/usr/bin/printf '\n scout-refresh: %s passed, %s failed\n\n' \
|
|
155
|
+
"$(green "$PASS")" "$([[ $FAIL -eq 0 ]] && green 0 || red "$FAIL")"
|
|
156
|
+
|
|
157
|
+
[[ $FAIL -eq 0 ]] && exit 0 || exit 1
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test-stale-dossier-refresh.sh — regression test for dossier freshness signals.
|
|
3
|
+
#
|
|
4
|
+
# Promise: researcher-build.sh emits a `last_refreshed:` ISO-8601 timestamp
|
|
5
|
+
# matching "now" (within 60 seconds). The 14-day staleness check is a
|
|
6
|
+
# downstream consumer concern (SKILL.md Step 0.5 + dossier_age helpers);
|
|
7
|
+
# this test validates the BUILDER side of the contract.
|
|
8
|
+
#
|
|
9
|
+
# Note: the actual "auto-refresh at 14 days" trigger logic lives in
|
|
10
|
+
# /contribute SKILL.md as agent instructions (not executable code). What
|
|
11
|
+
# we CAN test deterministically:
|
|
12
|
+
# 1. researcher-build.sh writes last_refreshed: to current time
|
|
13
|
+
# 2. an old timestamp is detectable as stale by simple shell math
|
|
14
|
+
# 3. the dossier path slug (owner__repo) matches what gates expect
|
|
15
|
+
#
|
|
16
|
+
# Usage: test-stale-dossier-refresh.sh [--verbose]
|
|
17
|
+
# Exit 0: all assertions hold. Exit 1: any failure.
|
|
18
|
+
|
|
19
|
+
set -uo pipefail
|
|
20
|
+
|
|
21
|
+
VERBOSE="${1:-}"
|
|
22
|
+
SYS="$HOME/.contribute-system"
|
|
23
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
24
|
+
TMPDIR=$(/usr/bin/mktemp -d)
|
|
25
|
+
trap 'rm -rf "$TMPDIR"' EXIT
|
|
26
|
+
|
|
27
|
+
PASS=0
|
|
28
|
+
FAIL=0
|
|
29
|
+
red() { /usr/bin/printf '\033[31m%s\033[0m' "$1"; }
|
|
30
|
+
green() { /usr/bin/printf '\033[32m%s\033[0m' "$1"; }
|
|
31
|
+
|
|
32
|
+
assert() {
|
|
33
|
+
local name="$1" expr="$2"
|
|
34
|
+
/usr/bin/printf ' %-60s ' "$name"
|
|
35
|
+
if eval "$expr" >/dev/null 2>&1; then
|
|
36
|
+
green "PASS"; /usr/bin/echo
|
|
37
|
+
PASS=$((PASS + 1))
|
|
38
|
+
else
|
|
39
|
+
red "FAIL"; /usr/bin/echo
|
|
40
|
+
[[ "$VERBOSE" == "--verbose" ]] && /usr/bin/printf ' expr: %s\n' "$expr" >&2
|
|
41
|
+
FAIL=$((FAIL + 1))
|
|
42
|
+
fi
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/usr/bin/printf '\n=== dossier freshness regression ===\n\n'
|
|
46
|
+
|
|
47
|
+
# --- TEST 1: researcher-build emits last_refreshed: matching now ---
|
|
48
|
+
DOSSIER="$TMPDIR/lingdojo__kana-dojo.md"
|
|
49
|
+
"$SCRIPT_DIR/researcher-build.sh" lingdojo/kana-dojo --no-link-follow > "$DOSSIER" 2>/dev/null
|
|
50
|
+
|
|
51
|
+
LAST_REFRESHED=$(/usr/bin/awk '/^last_refreshed:/{print $2; exit}' "$DOSSIER" 2>/dev/null)
|
|
52
|
+
NOW_EPOCH=$(/usr/bin/date +%s)
|
|
53
|
+
THEN_EPOCH=$(/usr/bin/date -d "$LAST_REFRESHED" +%s 2>/dev/null || /usr/bin/echo 0)
|
|
54
|
+
DELTA=$((NOW_EPOCH - THEN_EPOCH))
|
|
55
|
+
|
|
56
|
+
assert "1. dossier has last_refreshed frontmatter field" \
|
|
57
|
+
'[[ -n "$LAST_REFRESHED" ]]'
|
|
58
|
+
|
|
59
|
+
assert "2. last_refreshed is within 60 seconds of now" \
|
|
60
|
+
'[[ "$DELTA" -ge 0 && "$DELTA" -le 60 ]]'
|
|
61
|
+
|
|
62
|
+
# --- TEST 2: stale detection math works (synthetic 30-day-old timestamp) ---
|
|
63
|
+
THIRTY_DAYS_AGO=$(/usr/bin/date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ)
|
|
64
|
+
THIRTY_EPOCH=$(/usr/bin/date -d "$THIRTY_DAYS_AGO" +%s)
|
|
65
|
+
AGE_DAYS=$(( (NOW_EPOCH - THIRTY_EPOCH) / 86400 ))
|
|
66
|
+
|
|
67
|
+
assert "3. shell-math correctly identifies 30d-old as stale (>14d)" \
|
|
68
|
+
'[[ "$AGE_DAYS" -gt 14 ]]'
|
|
69
|
+
|
|
70
|
+
# --- TEST 3: dossier filename slug uses double underscore (matches gate expectations) ---
|
|
71
|
+
EXPECTED_SLUG="lingdojo__kana-dojo"
|
|
72
|
+
ACTUAL_SLUG=$(/usr/bin/basename "$DOSSIER" .md)
|
|
73
|
+
|
|
74
|
+
assert "4. dossier filename slug matches owner__repo convention" \
|
|
75
|
+
'[[ "$ACTUAL_SLUG" == "$EXPECTED_SLUG" ]]'
|
|
76
|
+
|
|
77
|
+
# --- TEST 4: the dossier has the manual sections that survive refresh ---
|
|
78
|
+
assert "5. dossier has Pet peeves section (manual, append-only)" \
|
|
79
|
+
'/usr/bin/grep -q "^## Pet peeves" "$DOSSIER"'
|
|
80
|
+
|
|
81
|
+
assert "6. dossier has Failure log section (manual, append-only)" \
|
|
82
|
+
'/usr/bin/grep -q "^## Failure log" "$DOSSIER"'
|
|
83
|
+
|
|
84
|
+
assert "7. dossier has Notes section (manual, append-only)" \
|
|
85
|
+
'/usr/bin/grep -q "^## Notes" "$DOSSIER"'
|
|
86
|
+
|
|
87
|
+
# --- TEST 5: dossier has issue_templates field (added 2026-05-03) ---
|
|
88
|
+
assert "8. dossier has issue_templates frontmatter (per skill-creator)" \
|
|
89
|
+
'/usr/bin/grep -q "^issue_templates:" "$DOSSIER"'
|
|
90
|
+
|
|
91
|
+
/usr/bin/echo
|
|
92
|
+
/usr/bin/printf '=== summary: %s passed · %s failed ===\n\n' \
|
|
93
|
+
"$(green "$PASS")" "$([ "$FAIL" -gt 0 ] && red "$FAIL" || /usr/bin/echo 0)"
|
|
94
|
+
|
|
95
|
+
[[ "$FAIL" -eq 0 ]] || exit 1
|
|
96
|
+
exit 0
|