From 8a9993106a4be9afc6bbcd319dc5f6dd3a0c674d Mon Sep 17 00:00:00 2001 From: Jim Cadden Date: Thu, 29 Oct 2026 09:50:06 -0400 Subject: [PATCH 1/6] feat: Show a REPOMAN banner on interactive sessions Interactive /repoman chat sessions should open with the ASCII header before orientation or any other reply. Signed-off-by: Jim Cadden Assisted-by: Cursor --- skills/repoman/SKILL.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/skills/repoman/SKILL.md b/skills/repoman/SKILL.md index 2ebf2b5..6058a79 100644 --- a/skills/repoman/SKILL.md +++ b/skills/repoman/SKILL.md @@ -126,6 +126,31 @@ options and do **not** pick for the user: - **B — Setup only:** complete and confirm setup, then stop. The user re-invokes `/repoman` to run. +## Startup banner + +On **interactive** `/repoman` invocations (a human-driven chat session — e.g. +`/repoman` or `/repoman:repoman`), open with this ASCII header as the first +user-facing content — before orientation, cold-start questions, activate/run +handoff, or any other reply. Skip the banner for non-interactive contexts +(scheduled agent runs, pipelines, CI). Print it verbatim in a fenced code +block so the box-drawing characters align in monospace: + +``` +██████╗ ███████╗ ██████╗ ██████╗ +██╔══██╗ ██╔════╝ ██╔══██╗ ██╔═══██╗ +██████╔╝ █████╗ ██████╔╝ ██║ ██║ +██╔══██╗ ██╔══╝ ██╔═══╝ ██║ ██║ +██║ ██║ ███████╗ ██║ ╚██████╔╝ +╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═════╝ + +███╗ ███╗ █████╗ ███╗ ██╗ +████╗ ████║ ██╔══██╗ ████╗ ██║ +██╔████╔██║ ███████║ ██╔██╗ ██║ +██║╚██╔╝██║ ██╔══██║ ██║╚██╗██║ +██║ ╚═╝ ██║ ██║ ██║ ██║ ╚████║ +╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═══╝ +``` + ## Welcome orientation (new-user acquisition) Whenever you reach a **no-intent branch** — a brand-new user with no setup, or a From 7a8fb1445da96d835133d54f2285ce084079eb97 Mon Sep 17 00:00:00 2001 From: Jim Cadden Date: Thu, 29 Oct 2026 11:44:00 -0400 Subject: [PATCH 2/6] feat: Mark agent posts and tighten the interactive banner A concrete skip list keeps the banner off scheduled and headless runs, and GitHub or Slack messages the agent posts end with a RepoMan footer. Signed-off-by: Jim Cadden Assisted-by: Cursor --- skills/github-pr-review/SKILL.md | 4 +++- skills/github-weekly-report/SKILL.md | 2 ++ skills/github-weekly-report/scripts/report.py | 2 ++ skills/repoman/SKILL.md | 11 +++++------ 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/skills/github-pr-review/SKILL.md b/skills/github-pr-review/SKILL.md index a9e56af..de88bc7 100644 --- a/skills/github-pr-review/SKILL.md +++ b/skills/github-pr-review/SKILL.md @@ -409,6 +409,8 @@ Do NOT include praise comments — they clutter the review without adding value. **Agent/IDE config (.claude/.vscode)**: none / FLAGGED (list paths) **Commits**: N commits, all signed-off: yes/no **CI status**: passing/failing/pending + +Generated by RepoMan ``` Map `` to its §1.6 trust tier so the line is self-explanatory: @@ -470,7 +472,7 @@ step 1. ## Phase 5: Submit Review -After user approves, post the review via GitHub API. +After user approves, post the review via GitHub API. The review `body`, and any Slack post of it, ends with `Generated by RepoMan`. Inline comments do not. ### 5.1 Post Review with Inline Comments diff --git a/skills/github-weekly-report/SKILL.md b/skills/github-weekly-report/SKILL.md index 3237e50..31176df 100644 --- a/skills/github-weekly-report/SKILL.md +++ b/skills/github-weekly-report/SKILL.md @@ -142,6 +142,8 @@ The cron job runs weekly on Mondays at 05:01 UTC. The agent: 4. Closes previous weekly report issues 5. Posts the final report as a new GitHub issue +The posted GitHub issue, and any Slack post of the report, ends with `Generated by RepoMan`. + ## See Also - GitHub CLI docs: https://cli.github.com/manual/ diff --git a/skills/github-weekly-report/scripts/report.py b/skills/github-weekly-report/scripts/report.py index 369bf99..64511e1 100644 --- a/skills/github-weekly-report/scripts/report.py +++ b/skills/github-weekly-report/scripts/report.py @@ -445,6 +445,8 @@ def generate_report(org, since, until, enhanced=False, repos=None): lines.append(f"| [#{issue['number']}](https://github.com/{d['owner']}/{d['name']}/issues/{issue['number']}) | {t} | {created_date} |") lines.append("") + lines += ["", "Generated by RepoMan"] + return '\n'.join(lines), repos_data, epic_data def build_json_output(since, until, repos_data, epic_data=None): diff --git a/skills/repoman/SKILL.md b/skills/repoman/SKILL.md index 6058a79..0e67225 100644 --- a/skills/repoman/SKILL.md +++ b/skills/repoman/SKILL.md @@ -128,12 +128,7 @@ options and do **not** pick for the user: ## Startup banner -On **interactive** `/repoman` invocations (a human-driven chat session — e.g. -`/repoman` or `/repoman:repoman`), open with this ASCII header as the first -user-facing content — before orientation, cold-start questions, activate/run -handoff, or any other reply. Skip the banner for non-interactive contexts -(scheduled agent runs, pipelines, CI). Print it verbatim in a fenced code -block so the box-drawing characters align in monospace: +On a human `/repoman` turn, print this header first, verbatim, in a fenced code block. Skip it when no human is in the turn: schedule/cron, headless `-p`, pipeline, or CI. ``` ██████╗ ███████╗ ██████╗ ██████╗ @@ -192,6 +187,10 @@ they want in a follow-up turn. The orientation makes **no** `~/.repoman/` writes; it is pure presentation over inferred state. +## Outbound footer + +End every GitHub or Slack message you post with `Generated by RepoMan`. + ## Presentation (naming, pacing, and choice UI) These rules shape every user-facing prompt. They never change what gets written From 3e69bdf2c630acdb648f4bcc78216dca782d4358 Mon Sep 17 00:00:00 2001 From: Jim Cadden Date: Thu, 29 Oct 2026 13:53:38 -0400 Subject: [PATCH 3/6] feat: Use the pinned-SHA footer for pr-review and weekly-report The scripts append skill_attribution, so a posted review or report carries one provenance line and the skill text does not add a second. Signed-off-by: Jim Cadden Assisted-by: Cursor --- skills/github-pr-review/SKILL.md | 4 +- skills/github-weekly-report/SKILL.md | 4 +- skills/github-weekly-report/scripts/report.py | 2 - skills/repoman/SKILL.md | 2 +- skills/repoman/scripts/github-api.sh | 3 +- skills/repoman/scripts/pr-review-fixer.sh | 13 +- skills/repoman/scripts/pr-review-scanner.sh | 3 + skills/repoman/scripts/weekly-report.sh | 12 +- tests/test-skill-attribution.sh | 135 ++++++++++++++++++ 9 files changed, 166 insertions(+), 12 deletions(-) create mode 100644 tests/test-skill-attribution.sh diff --git a/skills/github-pr-review/SKILL.md b/skills/github-pr-review/SKILL.md index de88bc7..207523d 100644 --- a/skills/github-pr-review/SKILL.md +++ b/skills/github-pr-review/SKILL.md @@ -409,8 +409,6 @@ Do NOT include praise comments — they clutter the review without adding value. **Agent/IDE config (.claude/.vscode)**: none / FLAGGED (list paths) **Commits**: N commits, all signed-off: yes/no **CI status**: passing/failing/pending - -Generated by RepoMan ``` Map `` to its §1.6 trust tier so the line is self-explanatory: @@ -472,7 +470,7 @@ step 1. ## Phase 5: Submit Review -After user approves, post the review via GitHub API. The review `body`, and any Slack post of it, ends with `Generated by RepoMan`. Inline comments do not. +After user approves, post the review via GitHub API. The review `body`, and any Slack post of it, ends with the exact line from `pr-review-fixer.sh footer`. Do not add a second footer. Inline comments do not. ### 5.1 Post Review with Inline Comments diff --git a/skills/github-weekly-report/SKILL.md b/skills/github-weekly-report/SKILL.md index 31176df..52fa59d 100644 --- a/skills/github-weekly-report/SKILL.md +++ b/skills/github-weekly-report/SKILL.md @@ -136,13 +136,13 @@ Append a `## Action Items` section at the end of the report. Produce a prioritiz ## Automation The cron job runs weekly on Mondays at 05:01 UTC. The agent: -1. Runs `report.py` with `--json-output` +1. Runs `weekly-report.sh` with `--output` and `--json-output` 2. Reads the structured JSON and markdown 3. Enhances the Active Epics, Cross-Repo Highlights, and Action Items sections 4. Closes previous weekly report issues 5. Posts the final report as a new GitHub issue -The posted GitHub issue, and any Slack post of the report, ends with `Generated by RepoMan`. +`weekly-report.sh` already ends the report with the pinned-SHA attribution line. Leave that line last on the GitHub issue and any Slack post. Do not add a second footer. ## See Also diff --git a/skills/github-weekly-report/scripts/report.py b/skills/github-weekly-report/scripts/report.py index 64511e1..369bf99 100644 --- a/skills/github-weekly-report/scripts/report.py +++ b/skills/github-weekly-report/scripts/report.py @@ -445,8 +445,6 @@ def generate_report(org, since, until, enhanced=False, repos=None): lines.append(f"| [#{issue['number']}](https://github.com/{d['owner']}/{d['name']}/issues/{issue['number']}) | {t} | {created_date} |") lines.append("") - lines += ["", "Generated by RepoMan"] - return '\n'.join(lines), repos_data, epic_data def build_json_output(since, until, repos_data, epic_data=None): diff --git a/skills/repoman/SKILL.md b/skills/repoman/SKILL.md index 0e67225..810f494 100644 --- a/skills/repoman/SKILL.md +++ b/skills/repoman/SKILL.md @@ -189,7 +189,7 @@ inferred state. ## Outbound footer -End every GitHub or Slack message you post with `Generated by RepoMan`. +pr-review and weekly-report scripts append one pinned-SHA line (`Generated by RepoMan @ …`, or the `blob/main` fallback). Post that text as-is. Do not add a second footer. Inline review comments stay footer-free. ## Presentation (naming, pacing, and choice UI) diff --git a/skills/repoman/scripts/github-api.sh b/skills/repoman/scripts/github-api.sh index 9514e6a..fc855eb 100644 --- a/skills/repoman/scripts/github-api.sh +++ b/skills/repoman/scripts/github-api.sh @@ -297,7 +297,8 @@ resolve_skill_meta() { # Prints: a one-line "Generated by RepoMan ..." attribution footer. skill_attribution() { local skill_name="$1" - local skill_dir="${SCRIPT_DIR:-.}" + # REPOMAN_SKILL_DIR lets tests pin _meta outside the shared scripts dir. + local skill_dir="${REPOMAN_SKILL_DIR:-${SCRIPT_DIR:-.}}" local sha sha=$(resolve_skill_meta "$skill_dir" "$skill_name") diff --git a/skills/repoman/scripts/pr-review-fixer.sh b/skills/repoman/scripts/pr-review-fixer.sh index 3568623..9eff0b5 100755 --- a/skills/repoman/scripts/pr-review-fixer.sh +++ b/skills/repoman/scripts/pr-review-fixer.sh @@ -25,7 +25,7 @@ COMMAND="" while [[ $# -gt 0 ]]; do case $1 in - begin|finalize) COMMAND="$1"; shift ;; + begin|finalize|footer) COMMAND="$1"; shift ;; --reports-dir) REPORTS_DIR="$2"; shift 2 ;; --verbose) VERBOSE=true; shift ;; --dry-run) DRY_RUN=true; shift ;; @@ -45,6 +45,7 @@ USAGE: COMMANDS: begin Read latest.json, mark eligible PRs as in_progress, output queue finalize Check reviews API, move reviewed PRs to reviewed state + footer Print the pinned-SHA attribution line for the review body OPTIONS: --reports-dir DIR Reports directory (default: $REPORTS_DIR or ~/reports/pr-review) @@ -67,10 +68,18 @@ USAGE fi if [ -z "$COMMAND" ]; then - echo "ERROR: command required (begin or finalize). Use --help for usage." >&2 + echo "ERROR: command required (begin, finalize, or footer). Use --help for usage." >&2 exit 1 fi +# Pin provenance (6a: create-only _meta, blob/main fallback). `footer` prints +# the line that goes on the review body and its Slack post, not inline comments. +SKILL_FOOTER=$(skill_attribution "github-pr-review") +if [ "$COMMAND" = "footer" ]; then + printf '%s\n' "$SKILL_FOOTER" + exit 0 +fi + # --- Setup --- setup_workspace "pr-review-fixer" TMPDIR="$PROGRAM_TMPDIR" diff --git a/skills/repoman/scripts/pr-review-scanner.sh b/skills/repoman/scripts/pr-review-scanner.sh index 9d5e2a1..543e06c 100755 --- a/skills/repoman/scripts/pr-review-scanner.sh +++ b/skills/repoman/scripts/pr-review-scanner.sh @@ -58,6 +58,9 @@ USAGE exit 0 fi +# Pin provenance on startup (6a). A resolution failure must not fail the scan. +skill_attribution "github-pr-review" >/dev/null + # Resolve the enrolled repo set from ~/.repoman/repos.json (owner/name tuples). # repoman_config is not needed here -- the scanner queries GitHub directly and # writes no fork, so it needs no REPOS_DIR/FORK_OWNER. diff --git a/skills/repoman/scripts/weekly-report.sh b/skills/repoman/scripts/weekly-report.sh index 512a3a0..be38bfd 100755 --- a/skills/repoman/scripts/weekly-report.sh +++ b/skills/repoman/scripts/weekly-report.sh @@ -83,6 +83,10 @@ if [ ! -f "$REPORT_PY" ]; then exit 1 fi +# Pin provenance (6a) and append that exact line to the report that gets posted. +# report.py stays footer-free so this is the only attribution line. +SKILL_FOOTER=$(skill_attribution "github-weekly-report") + # Build args. $repos is intentionally unquoted so each owner/name line becomes a # separate --repos value; repo slugs never contain whitespace. No --org: the # enrolled set can span owners, so each entry is owner-qualified and report.py @@ -94,4 +98,10 @@ set -- --repos $repos [ -n "$OUTPUT" ] && set -- "$@" --output "$OUTPUT" [ -n "$JSON_OUTPUT" ] && set -- "$@" --json-output "$JSON_OUTPUT" -exec python3 "$REPORT_PY" "$@" +python3 "$REPORT_PY" "$@" + +if [ -n "$OUTPUT" ]; then + printf '\n%s\n' "$SKILL_FOOTER" >> "$OUTPUT" +else + printf '\n%s\n' "$SKILL_FOOTER" +fi diff --git a/tests/test-skill-attribution.sh b/tests/test-skill-attribution.sh new file mode 100644 index 0000000..b377c5c --- /dev/null +++ b/tests/test-skill-attribution.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +# Pinned-SHA footer for the two Phase 6 consumers that were deferred: +# pr-review (pr-review-fixer.sh footer) and weekly-report (appended to the +# report). Covers the 6a _meta rules: write on success, write nothing on +# failure, never overwrite a valid meta file. +set -uo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +SCRIPTS="$ROOT/skills/repoman/scripts" +# shellcheck disable=SC1091 +source "$SCRIPTS/program-lib.sh" + +fail=0 +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +FAKE_SHA="85449beabc1234567890abcdef1234567890abcd" +PINNED="deadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + +check() { + local name="$1" cond="$2" + if eval "$cond"; then + echo "PASS: $name" + else + echo "FAIL: $name" + fail=1 + fi +} + +for skill in github-pr-review github-weekly-report; do + dir="$TMP/ok-$skill" + mkdir -p "$dir" + gh() { printf '%s\n' "$FAKE_SHA"; } + sha=$(SKILL_SOURCE_REPO="rossoctl/agent-skills" REPOMAN_SKILL_DIR="$dir" \ + resolve_skill_meta "$dir" "$skill") + check "$skill meta written" "[ \"$sha\" = \"$FAKE_SHA\" ] && [ -f \"$dir/_meta.$skill.json\" ]" + foot=$(SKILL_SOURCE_REPO="rossoctl/agent-skills" REPOMAN_SKILL_DIR="$dir" \ + skill_attribution "$skill") + check "$skill pinned footer" "case \"\$foot\" in *\"Generated by RepoMan $skill@${FAKE_SHA:0:7}\"*\"blob/$FAKE_SHA/skills/$skill/SKILL.md\"*) true ;; *) false ;; esac" + + dirf="$TMP/fail-$skill" + mkdir -p "$dirf" + gh() { return 1; } + sha=$(SKILL_SOURCE_REPO="rossoctl/agent-skills" REPOMAN_SKILL_DIR="$dirf" \ + resolve_skill_meta "$dirf" "$skill" 2>/dev/null) + foot=$(SKILL_SOURCE_REPO="rossoctl/agent-skills" REPOMAN_SKILL_DIR="$dirf" \ + skill_attribution "$skill" 2>/dev/null) + check "$skill failure writes nothing" "[ -z \"$sha\" ] && [ ! -f \"$dirf/_meta.$skill.json\" ]" + check "$skill blob/main fallback" "case \"\$foot\" in *\"blob/main/skills/$skill/SKILL.md\"*) true ;; *) false ;; esac" + check "$skill fallback has no @sha" "case \"\$foot\" in *@*) false ;; *) true ;; esac" + + dirp="$TMP/pin-$skill" + mkdir -p "$dirp" + printf '{"version":"%s","source":"rossoctl/agent-skills","installed_at":"2026-01-01"}\n' \ + "$PINNED" > "$dirp/_meta.$skill.json" + called="$TMP/called-$skill" + : > "$called" + gh() { echo called >> "$called"; printf '%s\n' "$FAKE_SHA"; } + sha=$(SKILL_SOURCE_REPO="rossoctl/agent-skills" REPOMAN_SKILL_DIR="$dirp" \ + resolve_skill_meta "$dirp" "$skill") + check "$skill existing meta kept" "[ \"$sha\" = \"$PINNED\" ] && [ ! -s \"$called\" ]" +done +unset -f gh + +# Subprocess gh, so the entry points pick up the stub instead of a real call. +mkdir -p "$TMP/bin" +cat > "$TMP/bin/gh" << 'EOF' +#!/bin/sh +if [ -n "${GH_LOG:-}" ]; then + echo "$@" >> "$GH_LOG" +fi +if [ "${GH_FAIL:-}" = "1" ]; then + echo "rate limited" >&2 + exit 1 +fi +printf '%s\n' "$GH_SHA" +EOF +chmod +x "$TMP/bin/gh" +export PATH="$TMP/bin:$PATH" +export GH_SHA="$FAKE_SHA" +export GH_LOG="$TMP/gh.log" +export SKILL_SOURCE_REPO="rossoctl/agent-skills" +: > "$GH_LOG" + +export REPOMAN_SKILL_DIR="$TMP/fixer" +mkdir -p "$REPOMAN_SKILL_DIR" +foot=$(bash "$SCRIPTS/pr-review-fixer.sh" footer) +case "$foot" in + *"Generated by RepoMan github-pr-review@${FAKE_SHA:0:7}"*"blob/$FAKE_SHA/skills/github-pr-review/SKILL.md"*) + echo "PASS: fixer footer line" ;; + *) echo "FAIL: fixer footer line: $foot"; fail=1 ;; +esac +check "fixer writes _meta" "[ -f \"$REPOMAN_SKILL_DIR/_meta.github-pr-review.json\" ]" +: > "$GH_LOG" +bash "$SCRIPTS/pr-review-fixer.sh" footer >/dev/null +check "fixer does not refresh a valid _meta" "[ ! -s \"$GH_LOG\" ]" + +export REPOMAN_REPOS_FILE="$TMP/repos.json" +printf '[{"owner":"acme","name":"widgets"}]\n' > "$REPOMAN_REPOS_FILE" +export REPORT_PY="$TMP/report.py" +cat > "$REPORT_PY" << 'PY' +import sys +args = sys.argv[1:] +if "--output" in args: + open(args[args.index("--output") + 1], "w").write("# Weekly\nbody\n") +else: + sys.stdout.write("# Weekly\nbody\n") +PY +export REPOMAN_SKILL_DIR="$TMP/weekly" +mkdir -p "$REPOMAN_SKILL_DIR" +: > "$GH_LOG" +out="$TMP/report.md" +bash "$SCRIPTS/weekly-report.sh" --output "$out" >/dev/null +last=$(tail -n 1 "$out") +check "weekly report keeps its body" "grep -q '# Weekly' \"$out\"" +case "$last" in + *"Generated by RepoMan github-weekly-report@${FAKE_SHA:0:7}"*"blob/$FAKE_SHA/skills/github-weekly-report/SKILL.md"*) + echo "PASS: weekly report ends with pinned footer" ;; + *) echo "FAIL: weekly report ends with pinned footer: $last"; fail=1 ;; +esac +check "weekly writes its own _meta" "[ -f \"$REPOMAN_SKILL_DIR/_meta.github-weekly-report.json\" ]" +check "report.py does not add a second footer" "! grep -q 'Generated by RepoMan' \"$ROOT/skills/github-weekly-report/scripts/report.py\"" + +export GH_FAIL=1 +export REPOMAN_SKILL_DIR="$TMP/weekly-fail" +mkdir -p "$REPOMAN_SKILL_DIR" +outf="$TMP/report-fail.md" +bash "$SCRIPTS/weekly-report.sh" --output "$outf" >/dev/null 2>&1 +lastf=$(tail -n 1 "$outf") +case "$lastf" in + *"blob/main/skills/github-weekly-report/SKILL.md"*) + echo "PASS: weekly failure uses blob/main" ;; + *) echo "FAIL: weekly failure uses blob/main: $lastf"; fail=1 ;; +esac +check "weekly failure writes no _meta" "[ ! -f \"$REPOMAN_SKILL_DIR/_meta.github-weekly-report.json\" ]" + +[ "$fail" -eq 0 ] && echo "PASS: pr-review and weekly-report attribution" || exit 1 From 34293d945efed222bb7b2c0ce02e876bddbf140f Mon Sep 17 00:00:00 2001 From: Jim Cadden Date: Thu, 29 Oct 2026 15:04:00 -0400 Subject: [PATCH 4/6] revert: Drop the vendored pinned-SHA footer edits Those scripts are copied from rossoctl/automation and would be overwritten on the next re-vendor. The footer change lives there instead. Signed-off-by: Jim Cadden --- skills/github-pr-review/SKILL.md | 4 +- skills/github-weekly-report/SKILL.md | 4 +- skills/github-weekly-report/scripts/report.py | 2 + skills/repoman/SKILL.md | 2 +- skills/repoman/scripts/github-api.sh | 3 +- skills/repoman/scripts/pr-review-fixer.sh | 13 +- skills/repoman/scripts/pr-review-scanner.sh | 3 - skills/repoman/scripts/weekly-report.sh | 12 +- tests/test-skill-attribution.sh | 135 ------------------ 9 files changed, 12 insertions(+), 166 deletions(-) delete mode 100644 tests/test-skill-attribution.sh diff --git a/skills/github-pr-review/SKILL.md b/skills/github-pr-review/SKILL.md index 207523d..de88bc7 100644 --- a/skills/github-pr-review/SKILL.md +++ b/skills/github-pr-review/SKILL.md @@ -409,6 +409,8 @@ Do NOT include praise comments — they clutter the review without adding value. **Agent/IDE config (.claude/.vscode)**: none / FLAGGED (list paths) **Commits**: N commits, all signed-off: yes/no **CI status**: passing/failing/pending + +Generated by RepoMan ``` Map `` to its §1.6 trust tier so the line is self-explanatory: @@ -470,7 +472,7 @@ step 1. ## Phase 5: Submit Review -After user approves, post the review via GitHub API. The review `body`, and any Slack post of it, ends with the exact line from `pr-review-fixer.sh footer`. Do not add a second footer. Inline comments do not. +After user approves, post the review via GitHub API. The review `body`, and any Slack post of it, ends with `Generated by RepoMan`. Inline comments do not. ### 5.1 Post Review with Inline Comments diff --git a/skills/github-weekly-report/SKILL.md b/skills/github-weekly-report/SKILL.md index 52fa59d..31176df 100644 --- a/skills/github-weekly-report/SKILL.md +++ b/skills/github-weekly-report/SKILL.md @@ -136,13 +136,13 @@ Append a `## Action Items` section at the end of the report. Produce a prioritiz ## Automation The cron job runs weekly on Mondays at 05:01 UTC. The agent: -1. Runs `weekly-report.sh` with `--output` and `--json-output` +1. Runs `report.py` with `--json-output` 2. Reads the structured JSON and markdown 3. Enhances the Active Epics, Cross-Repo Highlights, and Action Items sections 4. Closes previous weekly report issues 5. Posts the final report as a new GitHub issue -`weekly-report.sh` already ends the report with the pinned-SHA attribution line. Leave that line last on the GitHub issue and any Slack post. Do not add a second footer. +The posted GitHub issue, and any Slack post of the report, ends with `Generated by RepoMan`. ## See Also diff --git a/skills/github-weekly-report/scripts/report.py b/skills/github-weekly-report/scripts/report.py index 369bf99..64511e1 100644 --- a/skills/github-weekly-report/scripts/report.py +++ b/skills/github-weekly-report/scripts/report.py @@ -445,6 +445,8 @@ def generate_report(org, since, until, enhanced=False, repos=None): lines.append(f"| [#{issue['number']}](https://github.com/{d['owner']}/{d['name']}/issues/{issue['number']}) | {t} | {created_date} |") lines.append("") + lines += ["", "Generated by RepoMan"] + return '\n'.join(lines), repos_data, epic_data def build_json_output(since, until, repos_data, epic_data=None): diff --git a/skills/repoman/SKILL.md b/skills/repoman/SKILL.md index 810f494..0e67225 100644 --- a/skills/repoman/SKILL.md +++ b/skills/repoman/SKILL.md @@ -189,7 +189,7 @@ inferred state. ## Outbound footer -pr-review and weekly-report scripts append one pinned-SHA line (`Generated by RepoMan @ …`, or the `blob/main` fallback). Post that text as-is. Do not add a second footer. Inline review comments stay footer-free. +End every GitHub or Slack message you post with `Generated by RepoMan`. ## Presentation (naming, pacing, and choice UI) diff --git a/skills/repoman/scripts/github-api.sh b/skills/repoman/scripts/github-api.sh index fc855eb..9514e6a 100644 --- a/skills/repoman/scripts/github-api.sh +++ b/skills/repoman/scripts/github-api.sh @@ -297,8 +297,7 @@ resolve_skill_meta() { # Prints: a one-line "Generated by RepoMan ..." attribution footer. skill_attribution() { local skill_name="$1" - # REPOMAN_SKILL_DIR lets tests pin _meta outside the shared scripts dir. - local skill_dir="${REPOMAN_SKILL_DIR:-${SCRIPT_DIR:-.}}" + local skill_dir="${SCRIPT_DIR:-.}" local sha sha=$(resolve_skill_meta "$skill_dir" "$skill_name") diff --git a/skills/repoman/scripts/pr-review-fixer.sh b/skills/repoman/scripts/pr-review-fixer.sh index 9eff0b5..3568623 100755 --- a/skills/repoman/scripts/pr-review-fixer.sh +++ b/skills/repoman/scripts/pr-review-fixer.sh @@ -25,7 +25,7 @@ COMMAND="" while [[ $# -gt 0 ]]; do case $1 in - begin|finalize|footer) COMMAND="$1"; shift ;; + begin|finalize) COMMAND="$1"; shift ;; --reports-dir) REPORTS_DIR="$2"; shift 2 ;; --verbose) VERBOSE=true; shift ;; --dry-run) DRY_RUN=true; shift ;; @@ -45,7 +45,6 @@ USAGE: COMMANDS: begin Read latest.json, mark eligible PRs as in_progress, output queue finalize Check reviews API, move reviewed PRs to reviewed state - footer Print the pinned-SHA attribution line for the review body OPTIONS: --reports-dir DIR Reports directory (default: $REPORTS_DIR or ~/reports/pr-review) @@ -68,18 +67,10 @@ USAGE fi if [ -z "$COMMAND" ]; then - echo "ERROR: command required (begin, finalize, or footer). Use --help for usage." >&2 + echo "ERROR: command required (begin or finalize). Use --help for usage." >&2 exit 1 fi -# Pin provenance (6a: create-only _meta, blob/main fallback). `footer` prints -# the line that goes on the review body and its Slack post, not inline comments. -SKILL_FOOTER=$(skill_attribution "github-pr-review") -if [ "$COMMAND" = "footer" ]; then - printf '%s\n' "$SKILL_FOOTER" - exit 0 -fi - # --- Setup --- setup_workspace "pr-review-fixer" TMPDIR="$PROGRAM_TMPDIR" diff --git a/skills/repoman/scripts/pr-review-scanner.sh b/skills/repoman/scripts/pr-review-scanner.sh index 543e06c..9d5e2a1 100755 --- a/skills/repoman/scripts/pr-review-scanner.sh +++ b/skills/repoman/scripts/pr-review-scanner.sh @@ -58,9 +58,6 @@ USAGE exit 0 fi -# Pin provenance on startup (6a). A resolution failure must not fail the scan. -skill_attribution "github-pr-review" >/dev/null - # Resolve the enrolled repo set from ~/.repoman/repos.json (owner/name tuples). # repoman_config is not needed here -- the scanner queries GitHub directly and # writes no fork, so it needs no REPOS_DIR/FORK_OWNER. diff --git a/skills/repoman/scripts/weekly-report.sh b/skills/repoman/scripts/weekly-report.sh index be38bfd..512a3a0 100755 --- a/skills/repoman/scripts/weekly-report.sh +++ b/skills/repoman/scripts/weekly-report.sh @@ -83,10 +83,6 @@ if [ ! -f "$REPORT_PY" ]; then exit 1 fi -# Pin provenance (6a) and append that exact line to the report that gets posted. -# report.py stays footer-free so this is the only attribution line. -SKILL_FOOTER=$(skill_attribution "github-weekly-report") - # Build args. $repos is intentionally unquoted so each owner/name line becomes a # separate --repos value; repo slugs never contain whitespace. No --org: the # enrolled set can span owners, so each entry is owner-qualified and report.py @@ -98,10 +94,4 @@ set -- --repos $repos [ -n "$OUTPUT" ] && set -- "$@" --output "$OUTPUT" [ -n "$JSON_OUTPUT" ] && set -- "$@" --json-output "$JSON_OUTPUT" -python3 "$REPORT_PY" "$@" - -if [ -n "$OUTPUT" ]; then - printf '\n%s\n' "$SKILL_FOOTER" >> "$OUTPUT" -else - printf '\n%s\n' "$SKILL_FOOTER" -fi +exec python3 "$REPORT_PY" "$@" diff --git a/tests/test-skill-attribution.sh b/tests/test-skill-attribution.sh deleted file mode 100644 index b377c5c..0000000 --- a/tests/test-skill-attribution.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env bash -# Pinned-SHA footer for the two Phase 6 consumers that were deferred: -# pr-review (pr-review-fixer.sh footer) and weekly-report (appended to the -# report). Covers the 6a _meta rules: write on success, write nothing on -# failure, never overwrite a valid meta file. -set -uo pipefail -ROOT="$(cd "$(dirname "$0")/.." && pwd)" -SCRIPTS="$ROOT/skills/repoman/scripts" -# shellcheck disable=SC1091 -source "$SCRIPTS/program-lib.sh" - -fail=0 -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -FAKE_SHA="85449beabc1234567890abcdef1234567890abcd" -PINNED="deadbeefdeadbeefdeadbeefdeadbeefdeadbeef" - -check() { - local name="$1" cond="$2" - if eval "$cond"; then - echo "PASS: $name" - else - echo "FAIL: $name" - fail=1 - fi -} - -for skill in github-pr-review github-weekly-report; do - dir="$TMP/ok-$skill" - mkdir -p "$dir" - gh() { printf '%s\n' "$FAKE_SHA"; } - sha=$(SKILL_SOURCE_REPO="rossoctl/agent-skills" REPOMAN_SKILL_DIR="$dir" \ - resolve_skill_meta "$dir" "$skill") - check "$skill meta written" "[ \"$sha\" = \"$FAKE_SHA\" ] && [ -f \"$dir/_meta.$skill.json\" ]" - foot=$(SKILL_SOURCE_REPO="rossoctl/agent-skills" REPOMAN_SKILL_DIR="$dir" \ - skill_attribution "$skill") - check "$skill pinned footer" "case \"\$foot\" in *\"Generated by RepoMan $skill@${FAKE_SHA:0:7}\"*\"blob/$FAKE_SHA/skills/$skill/SKILL.md\"*) true ;; *) false ;; esac" - - dirf="$TMP/fail-$skill" - mkdir -p "$dirf" - gh() { return 1; } - sha=$(SKILL_SOURCE_REPO="rossoctl/agent-skills" REPOMAN_SKILL_DIR="$dirf" \ - resolve_skill_meta "$dirf" "$skill" 2>/dev/null) - foot=$(SKILL_SOURCE_REPO="rossoctl/agent-skills" REPOMAN_SKILL_DIR="$dirf" \ - skill_attribution "$skill" 2>/dev/null) - check "$skill failure writes nothing" "[ -z \"$sha\" ] && [ ! -f \"$dirf/_meta.$skill.json\" ]" - check "$skill blob/main fallback" "case \"\$foot\" in *\"blob/main/skills/$skill/SKILL.md\"*) true ;; *) false ;; esac" - check "$skill fallback has no @sha" "case \"\$foot\" in *@*) false ;; *) true ;; esac" - - dirp="$TMP/pin-$skill" - mkdir -p "$dirp" - printf '{"version":"%s","source":"rossoctl/agent-skills","installed_at":"2026-01-01"}\n' \ - "$PINNED" > "$dirp/_meta.$skill.json" - called="$TMP/called-$skill" - : > "$called" - gh() { echo called >> "$called"; printf '%s\n' "$FAKE_SHA"; } - sha=$(SKILL_SOURCE_REPO="rossoctl/agent-skills" REPOMAN_SKILL_DIR="$dirp" \ - resolve_skill_meta "$dirp" "$skill") - check "$skill existing meta kept" "[ \"$sha\" = \"$PINNED\" ] && [ ! -s \"$called\" ]" -done -unset -f gh - -# Subprocess gh, so the entry points pick up the stub instead of a real call. -mkdir -p "$TMP/bin" -cat > "$TMP/bin/gh" << 'EOF' -#!/bin/sh -if [ -n "${GH_LOG:-}" ]; then - echo "$@" >> "$GH_LOG" -fi -if [ "${GH_FAIL:-}" = "1" ]; then - echo "rate limited" >&2 - exit 1 -fi -printf '%s\n' "$GH_SHA" -EOF -chmod +x "$TMP/bin/gh" -export PATH="$TMP/bin:$PATH" -export GH_SHA="$FAKE_SHA" -export GH_LOG="$TMP/gh.log" -export SKILL_SOURCE_REPO="rossoctl/agent-skills" -: > "$GH_LOG" - -export REPOMAN_SKILL_DIR="$TMP/fixer" -mkdir -p "$REPOMAN_SKILL_DIR" -foot=$(bash "$SCRIPTS/pr-review-fixer.sh" footer) -case "$foot" in - *"Generated by RepoMan github-pr-review@${FAKE_SHA:0:7}"*"blob/$FAKE_SHA/skills/github-pr-review/SKILL.md"*) - echo "PASS: fixer footer line" ;; - *) echo "FAIL: fixer footer line: $foot"; fail=1 ;; -esac -check "fixer writes _meta" "[ -f \"$REPOMAN_SKILL_DIR/_meta.github-pr-review.json\" ]" -: > "$GH_LOG" -bash "$SCRIPTS/pr-review-fixer.sh" footer >/dev/null -check "fixer does not refresh a valid _meta" "[ ! -s \"$GH_LOG\" ]" - -export REPOMAN_REPOS_FILE="$TMP/repos.json" -printf '[{"owner":"acme","name":"widgets"}]\n' > "$REPOMAN_REPOS_FILE" -export REPORT_PY="$TMP/report.py" -cat > "$REPORT_PY" << 'PY' -import sys -args = sys.argv[1:] -if "--output" in args: - open(args[args.index("--output") + 1], "w").write("# Weekly\nbody\n") -else: - sys.stdout.write("# Weekly\nbody\n") -PY -export REPOMAN_SKILL_DIR="$TMP/weekly" -mkdir -p "$REPOMAN_SKILL_DIR" -: > "$GH_LOG" -out="$TMP/report.md" -bash "$SCRIPTS/weekly-report.sh" --output "$out" >/dev/null -last=$(tail -n 1 "$out") -check "weekly report keeps its body" "grep -q '# Weekly' \"$out\"" -case "$last" in - *"Generated by RepoMan github-weekly-report@${FAKE_SHA:0:7}"*"blob/$FAKE_SHA/skills/github-weekly-report/SKILL.md"*) - echo "PASS: weekly report ends with pinned footer" ;; - *) echo "FAIL: weekly report ends with pinned footer: $last"; fail=1 ;; -esac -check "weekly writes its own _meta" "[ -f \"$REPOMAN_SKILL_DIR/_meta.github-weekly-report.json\" ]" -check "report.py does not add a second footer" "! grep -q 'Generated by RepoMan' \"$ROOT/skills/github-weekly-report/scripts/report.py\"" - -export GH_FAIL=1 -export REPOMAN_SKILL_DIR="$TMP/weekly-fail" -mkdir -p "$REPOMAN_SKILL_DIR" -outf="$TMP/report-fail.md" -bash "$SCRIPTS/weekly-report.sh" --output "$outf" >/dev/null 2>&1 -lastf=$(tail -n 1 "$outf") -case "$lastf" in - *"blob/main/skills/github-weekly-report/SKILL.md"*) - echo "PASS: weekly failure uses blob/main" ;; - *) echo "FAIL: weekly failure uses blob/main: $lastf"; fail=1 ;; -esac -check "weekly failure writes no _meta" "[ ! -f \"$REPOMAN_SKILL_DIR/_meta.github-weekly-report.json\" ]" - -[ "$fail" -eq 0 ] && echo "PASS: pr-review and weekly-report attribution" || exit 1 From d8f588c4ae3682c7724f7e4624e137f795297b17 Mon Sep 17 00:00:00 2001 From: Jim Cadden Date: Thu, 29 Oct 2026 17:21:43 -0400 Subject: [PATCH 5/6] fix: Drop the vendored footer edits from this branch Engine scripts are overwritten on the next re-vendor, so the pinned-SHA footer stays in automation. This only stops the agent from adding its own footer. Signed-off-by: Jim Cadden --- skills/github-pr-review/SKILL.md | 4 +--- skills/github-weekly-report/SKILL.md | 2 +- skills/github-weekly-report/scripts/report.py | 2 -- skills/repoman/SKILL.md | 2 +- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/skills/github-pr-review/SKILL.md b/skills/github-pr-review/SKILL.md index de88bc7..31b9971 100644 --- a/skills/github-pr-review/SKILL.md +++ b/skills/github-pr-review/SKILL.md @@ -409,8 +409,6 @@ Do NOT include praise comments — they clutter the review without adding value. **Agent/IDE config (.claude/.vscode)**: none / FLAGGED (list paths) **Commits**: N commits, all signed-off: yes/no **CI status**: passing/failing/pending - -Generated by RepoMan ``` Map `` to its §1.6 trust tier so the line is self-explanatory: @@ -472,7 +470,7 @@ step 1. ## Phase 5: Submit Review -After user approves, post the review via GitHub API. The review `body`, and any Slack post of it, ends with `Generated by RepoMan`. Inline comments do not. +After user approves, post the review via GitHub API. Do not append a footer yourself. Inline comments do not get one. ### 5.1 Post Review with Inline Comments diff --git a/skills/github-weekly-report/SKILL.md b/skills/github-weekly-report/SKILL.md index 31176df..832c857 100644 --- a/skills/github-weekly-report/SKILL.md +++ b/skills/github-weekly-report/SKILL.md @@ -142,7 +142,7 @@ The cron job runs weekly on Mondays at 05:01 UTC. The agent: 4. Closes previous weekly report issues 5. Posts the final report as a new GitHub issue -The posted GitHub issue, and any Slack post of the report, ends with `Generated by RepoMan`. +Do not append a footer yourself. Leave any attribution line the report already has in place. ## See Also diff --git a/skills/github-weekly-report/scripts/report.py b/skills/github-weekly-report/scripts/report.py index 64511e1..369bf99 100644 --- a/skills/github-weekly-report/scripts/report.py +++ b/skills/github-weekly-report/scripts/report.py @@ -445,8 +445,6 @@ def generate_report(org, since, until, enhanced=False, repos=None): lines.append(f"| [#{issue['number']}](https://github.com/{d['owner']}/{d['name']}/issues/{issue['number']}) | {t} | {created_date} |") lines.append("") - lines += ["", "Generated by RepoMan"] - return '\n'.join(lines), repos_data, epic_data def build_json_output(since, until, repos_data, epic_data=None): diff --git a/skills/repoman/SKILL.md b/skills/repoman/SKILL.md index 0e67225..8fa114c 100644 --- a/skills/repoman/SKILL.md +++ b/skills/repoman/SKILL.md @@ -189,7 +189,7 @@ inferred state. ## Outbound footer -End every GitHub or Slack message you post with `Generated by RepoMan`. +Do not append a footer yourself. Program scripts add the attribution line. Inline review comments stay footer-free. ## Presentation (naming, pacing, and choice UI) From 8833d7849b79273c160aed15d6c5e959e081129a Mon Sep 17 00:00:00 2001 From: Jim Cadden Date: Fri, 30 Oct 2026 10:50:36 -0400 Subject: [PATCH 6/6] docs: Append the script footer on review bodies only The review body and its Slack post take the line pr-review-fixer.sh footer prints. Inline comments stay without one. Signed-off-by: Jim Cadden Assisted-by: Cursor --- skills/github-pr-review/SKILL.md | 2 +- skills/repoman/SKILL.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/skills/github-pr-review/SKILL.md b/skills/github-pr-review/SKILL.md index 31b9971..e6a4eb4 100644 --- a/skills/github-pr-review/SKILL.md +++ b/skills/github-pr-review/SKILL.md @@ -470,7 +470,7 @@ step 1. ## Phase 5: Submit Review -After user approves, post the review via GitHub API. Do not append a footer yourself. Inline comments do not get one. +After user approves, post the review via GitHub API. End the review body and its Slack post with the exact line from `pr-review-fixer.sh footer`. Inline comments do not get one. ### 5.1 Post Review with Inline Comments diff --git a/skills/repoman/SKILL.md b/skills/repoman/SKILL.md index 8fa114c..9674e0d 100644 --- a/skills/repoman/SKILL.md +++ b/skills/repoman/SKILL.md @@ -189,7 +189,7 @@ inferred state. ## Outbound footer -Do not append a footer yourself. Program scripts add the attribution line. Inline review comments stay footer-free. +Do not write your own footer. End a review body and its Slack post with the line from `pr-review-fixer.sh footer`. Leave a weekly report's attribution line as the script wrote it. Inline review comments stay footer-free. ## Presentation (naming, pacing, and choice UI)