tipalti 0.0.1__tar.gz

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 (46) hide show
  1. tipalti-0.0.1/.claude/skills/gh-issues/SKILL.md +45 -0
  2. tipalti-0.0.1/.claude/skills/gh-issues/scripts/gh-issues.sh +52 -0
  3. tipalti-0.0.1/.claude/skills/notebooklm/SKILL.md +47 -0
  4. tipalti-0.0.1/.claude/skills/notebooklm/scripts/get-repo-sources.sh +200 -0
  5. tipalti-0.0.1/.claude/skills/pr-review/SKILL.md +120 -0
  6. tipalti-0.0.1/.claude/skills/pr-review/scripts/portability-lint.sh +57 -0
  7. tipalti-0.0.1/.claude/skills/pr-review/scripts/pr-batch.sh +57 -0
  8. tipalti-0.0.1/.claude/skills/pr-review/scripts/pr-comments.sh +100 -0
  9. tipalti-0.0.1/.claude/skills/pr-review/scripts/pr-reply.sh +66 -0
  10. tipalti-0.0.1/.claude/skills/pr-review/scripts/pr-status.sh +161 -0
  11. tipalti-0.0.1/.claude/skills/pr-review/scripts/workflow.sh +99 -0
  12. tipalti-0.0.1/.claude/skills/pypi-maintainer/SKILL.md +75 -0
  13. tipalti-0.0.1/.claude/skills/pypi-maintainer/scripts/switch-source.sh +102 -0
  14. tipalti-0.0.1/.claude/skills/run-tests/SKILL.md +50 -0
  15. tipalti-0.0.1/.claude/skills/run-tests/scripts/test.sh +52 -0
  16. tipalti-0.0.1/.claude/skills/sonarclaude/SKILL.md +84 -0
  17. tipalti-0.0.1/.claude/skills/sonarclaude/scripts/sonar.sh +263 -0
  18. tipalti-0.0.1/.claude/skills/version-bump/SKILL.md +66 -0
  19. tipalti-0.0.1/.claude/skills/version-bump/scripts/bump.py +178 -0
  20. tipalti-0.0.1/.claude/skills.local.yaml.example +14 -0
  21. tipalti-0.0.1/.flake8 +7 -0
  22. tipalti-0.0.1/.github/workflows/publish.yml +88 -0
  23. tipalti-0.0.1/.github/workflows/tests.yml +114 -0
  24. tipalti-0.0.1/.gitignore +222 -0
  25. tipalti-0.0.1/.markdownlint-cli2.yaml +19 -0
  26. tipalti-0.0.1/CHANGELOG.md +15 -0
  27. tipalti-0.0.1/CLAUDE.md +88 -0
  28. tipalti-0.0.1/LICENSE +21 -0
  29. tipalti-0.0.1/PKG-INFO +63 -0
  30. tipalti-0.0.1/README.md +46 -0
  31. tipalti-0.0.1/pyproject.toml +70 -0
  32. tipalti-0.0.1/tests/test_cli_learn.py +39 -0
  33. tipalti-0.0.1/tests/test_cli_smoke.py +24 -0
  34. tipalti-0.0.1/tests/test_cli_whoami.py +21 -0
  35. tipalti-0.0.1/tipalti/__init__.py +13 -0
  36. tipalti-0.0.1/tipalti/__main__.py +10 -0
  37. tipalti-0.0.1/tipalti/cli/__init__.py +84 -0
  38. tipalti-0.0.1/tipalti/cli/_commands/__init__.py +0 -0
  39. tipalti-0.0.1/tipalti/cli/_commands/explain.py +38 -0
  40. tipalti-0.0.1/tipalti/cli/_commands/learn.py +82 -0
  41. tipalti-0.0.1/tipalti/cli/_commands/whoami.py +29 -0
  42. tipalti-0.0.1/tipalti/cli/_errors.py +41 -0
  43. tipalti-0.0.1/tipalti/cli/_output.py +53 -0
  44. tipalti-0.0.1/tipalti/explain/__init__.py +24 -0
  45. tipalti-0.0.1/tipalti/explain/catalog.py +88 -0
  46. tipalti-0.0.1/uv.lock +467 -0
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: gh-issues
3
+ description: >
4
+ Fetch GitHub issues with full body and comments. Use when checking issues,
5
+ reviewing bug reports, or the user says "check issues", "fetch issues",
6
+ "issue #N", or references GitHub issue numbers.
7
+ triggers:
8
+ - check issues
9
+ - fetch issues
10
+ - issue #
11
+ - github issues
12
+ - verify issues
13
+ ---
14
+
15
+ # GitHub Issues Skill
16
+
17
+ Fetch one or more GitHub issues with full body text and all comments.
18
+
19
+ ## Usage
20
+
21
+ ### Single issue
22
+
23
+ ```bash
24
+ bash .claude/skills/gh-issues/scripts/gh-issues.sh 191
25
+ ```
26
+
27
+ ### Range
28
+
29
+ ```bash
30
+ bash .claude/skills/gh-issues/scripts/gh-issues.sh 191-197
31
+ ```
32
+
33
+ ### Specific list
34
+
35
+ ```bash
36
+ bash .claude/skills/gh-issues/scripts/gh-issues.sh 191 195 197
37
+ ```
38
+
39
+ ### Explicit repo
40
+
41
+ ```bash
42
+ bash .claude/skills/gh-issues/scripts/gh-issues.sh --repo owner/repo 42-50
43
+ ```
44
+
45
+ Output is JSON per issue with: number, title, state, labels, body, and comments array.
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bash
2
+ # Fetch GitHub issues with full body and comments
3
+ # Usage: gh-issues.sh [RANGE|NUMBER] [--repo OWNER/REPO]
4
+ # gh-issues.sh 191-197 # range
5
+ # gh-issues.sh 191 # single
6
+ # gh-issues.sh 191 192 195 # list
7
+ # gh-issues.sh --repo foo/bar 5 # explicit repo
8
+
9
+ set -euo pipefail
10
+
11
+ REPO_FLAG=""
12
+ NUMBERS=()
13
+
14
+ while [[ $# -gt 0 ]]; do
15
+ case "$1" in
16
+ --repo)
17
+ if [[ $# -lt 2 || -z "$2" ]]; then
18
+ echo "Error: --repo requires a value (OWNER/REPO)" >&2
19
+ echo "Usage: gh-issues.sh [RANGE|NUMBER...] [--repo OWNER/REPO]" >&2
20
+ exit 1
21
+ fi
22
+ REPO_FLAG="--repo $2"
23
+ shift 2 ;;
24
+ *-*) # range like 191-197
25
+ IFS='-' read -r start end <<< "$1"
26
+ for ((i=start; i<=end; i++)); do NUMBERS+=("$i"); done
27
+ shift ;;
28
+ *) NUMBERS+=("$1"); shift ;;
29
+ esac
30
+ done
31
+
32
+ if [[ ${#NUMBERS[@]} -eq 0 ]]; then
33
+ echo "Usage: gh-issues.sh [RANGE|NUMBER...] [--repo OWNER/REPO]" >&2
34
+ exit 1
35
+ fi
36
+
37
+ for num in "${NUMBERS[@]}"; do
38
+ echo "========================================"
39
+ echo "ISSUE #${num}"
40
+ echo "========================================"
41
+ # shellcheck disable=SC2086
42
+ gh issue view "$num" $REPO_FLAG --json number,title,state,labels,body,comments \
43
+ --jq '{
44
+ number: .number,
45
+ title: .title,
46
+ state: .state,
47
+ labels: [.labels[].name],
48
+ body: .body,
49
+ comments: [.comments[] | {author: .author.login, body: .body}]
50
+ }' 2>/dev/null || echo "ERROR: Could not fetch issue #${num}"
51
+ echo
52
+ done
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: notebooklm
3
+ description: >
4
+ Generate GitHub links to repo documentation for NotebookLM ingestion.
5
+ Use when: the user wants to create a NotebookLM notebook about a project,
6
+ needs doc links for a repo, or says "notebooklm", "notebook sources",
7
+ "get repo sources", or "doc links".
8
+ ---
9
+
10
+ # NotebookLM — Repo Source Links
11
+
12
+ Generate GitHub blob URLs for all documentation in the current repo, ready to paste into Google NotebookLM as sources.
13
+
14
+ ## When to Use
15
+
16
+ - Creating a NotebookLM notebook about a project
17
+ - Gathering all doc links for a repo
18
+ - Exporting documentation URLs for any external tool
19
+
20
+ ## Usage
21
+
22
+ ```bash
23
+ # Categorized output (default)
24
+ bash .claude/skills/notebooklm/scripts/get-repo-sources.sh
25
+
26
+ # Plain URLs only (for copy-paste)
27
+ bash .claude/skills/notebooklm/scripts/get-repo-sources.sh --plain
28
+
29
+ # Include plans and specs
30
+ bash .claude/skills/notebooklm/scripts/get-repo-sources.sh --all
31
+
32
+ # Override branch
33
+ bash .claude/skills/notebooklm/scripts/get-repo-sources.sh --branch develop
34
+ ```
35
+
36
+ ## Options
37
+
38
+ | Flag | Default | Description |
39
+ |------|---------|-------------|
40
+ | `--all` | off | Include plans, specs, and changelogs |
41
+ | `--plain` | off | Output only URLs, one per line (no headers) |
42
+ | `--branch NAME` | auto-detect | Override the git branch used in URLs |
43
+
44
+ ## Requirements
45
+
46
+ - Must be run inside a git repository with a GitHub remote
47
+ - `git` CLI available
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env bash
2
+ # get-repo-sources — Generate GitHub URLs for repo docs, ready for NotebookLM
3
+ set -euo pipefail
4
+
5
+ # --- Defaults ---
6
+ INCLUDE_ALL=false
7
+ PLAIN=false
8
+ BRANCH=""
9
+
10
+ # --- Parse args ---
11
+ usage() {
12
+ echo "Usage: get-repo-sources.sh [--all] [--plain] [--branch NAME]"
13
+ echo " --all Include plans, specs, and changelogs"
14
+ echo " --plain Output plain URLs only (no headers)"
15
+ echo " --branch Override branch (default: auto-detect)"
16
+ }
17
+
18
+ while [[ $# -gt 0 ]]; do
19
+ case "$1" in
20
+ --all) INCLUDE_ALL=true; shift ;;
21
+ --plain) PLAIN=true; shift ;;
22
+ --branch)
23
+ if [[ $# -lt 2 || -z "$2" ]]; then
24
+ echo "Error: --branch requires a value" >&2
25
+ usage >&2
26
+ exit 1
27
+ fi
28
+ BRANCH="$2"
29
+ shift 2 ;;
30
+ -h|--help)
31
+ usage
32
+ exit 0
33
+ ;;
34
+ *) echo "Unknown option: $1" >&2; exit 1 ;;
35
+ esac
36
+ done
37
+
38
+ # --- Detect repo root ---
39
+ REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || {
40
+ echo "Error: not inside a git repository" >&2; exit 1
41
+ }
42
+ cd "$REPO_ROOT"
43
+
44
+ # --- Detect GitHub URL ---
45
+ REMOTE_URL="$(git remote get-url origin 2>/dev/null)" || {
46
+ echo "Error: no 'origin' remote found" >&2; exit 1
47
+ }
48
+
49
+ # Normalise to https://github.com/OWNER/REPO
50
+ GITHUB_URL="$REMOTE_URL"
51
+ GITHUB_URL="${GITHUB_URL%.git}"
52
+ if [[ "$GITHUB_URL" == git@github.com:* ]]; then
53
+ GITHUB_URL="https://github.com/${GITHUB_URL#git@github.com:}"
54
+ fi
55
+
56
+ # --- Detect branch (handle detached HEAD) ---
57
+ if [[ -z "$BRANCH" ]]; then
58
+ BRANCH="$(git branch --show-current 2>/dev/null || true)"
59
+ fi
60
+ if [[ -z "$BRANCH" ]]; then
61
+ echo "Error: cannot determine the current branch (detached HEAD?)." >&2
62
+ echo "Hint: pass --branch <name> with a branch that exists on origin." >&2
63
+ exit 1
64
+ fi
65
+
66
+ # --- Verify the remote ref exists locally ---
67
+ REMOTE_REF="origin/${BRANCH}"
68
+ if ! git show-ref --verify --quiet "refs/remotes/${REMOTE_REF}"; then
69
+ echo "Error: remote ref '${REMOTE_REF}' is not available locally." >&2
70
+ echo "Hint: run 'git fetch origin ${BRANCH}', push the branch first, or pass --branch with an existing remote branch name." >&2
71
+ exit 1
72
+ fi
73
+
74
+ BASE_URL="${GITHUB_URL}/blob/${BRANCH}"
75
+
76
+ # --- Collect all .md files from remote branch (only files that exist on GitHub) ---
77
+ ALL_FILES=$(git ls-tree -r --name-only "${REMOTE_REF}" \
78
+ | grep '\.md$' \
79
+ | grep -v -E '^(\.github/|\.claude/|\.pytest_cache/|node_modules/|\.venv/|__pycache__/|\.mypy_cache/)' \
80
+ | sort)
81
+
82
+ # --- Filter ---
83
+ filter_file() {
84
+ local f="$1"
85
+ # Always exclude dot-dirs and lock files
86
+ [[ "$f" == .github/* ]] && return 1
87
+
88
+ if [[ "$INCLUDE_ALL" == "false" ]]; then
89
+ # Exclude plans, specs, changelogs by default
90
+ [[ "$f" == *superpowers/plans/* ]] && return 1
91
+ [[ "$f" == *superpowers/specs/* ]] && return 1
92
+ [[ "$f" == CHANGELOG.md ]] && return 1
93
+ fi
94
+ return 0
95
+ }
96
+
97
+ FILTERED=()
98
+ while IFS= read -r f; do
99
+ [[ -z "$f" ]] && continue
100
+ if filter_file "$f"; then
101
+ FILTERED+=("$f")
102
+ fi
103
+ done <<< "$ALL_FILES"
104
+
105
+ if [[ ${#FILTERED[@]} -eq 0 ]]; then
106
+ echo "No documentation files found." >&2
107
+ exit 1
108
+ fi
109
+
110
+ # --- Categorise ---
111
+ declare -a CAT_CORE=() CAT_ARCH=() CAT_START=() CAT_FEATURES=() CAT_PROTOCOL=()
112
+ declare -a CAT_CLIENTS=() CAT_USECASES=() CAT_PACKAGES=() CAT_PLANS=() CAT_SPECS=() CAT_OTHER=()
113
+
114
+ categorise() {
115
+ local f="$1"
116
+ local base
117
+ base="$(basename "$f")"
118
+
119
+ # Path-based categories first (more specific)
120
+ case "$f" in
121
+ *protocol/*) CAT_PROTOCOL+=("$f"); return ;;
122
+ *clients/*) CAT_CLIENTS+=("$f"); return ;;
123
+ *use-cases/*) CAT_USECASES+=("$f"); return ;;
124
+ *packages/*) CAT_PACKAGES+=("$f"); return ;;
125
+ *superpowers/plans*) CAT_PLANS+=("$f"); return ;;
126
+ *superpowers/specs*) CAT_SPECS+=("$f"); return ;;
127
+ *plugins/*) CAT_OTHER+=("$f"); return ;;
128
+ *skills/*) CAT_OTHER+=("$f"); return ;;
129
+ esac
130
+
131
+ # Root-level core files (only match files at repo root)
132
+ case "$f" in
133
+ README.md|CLAUDE.md|index.md) CAT_CORE+=("$f"); return ;;
134
+ CHANGELOG.md) CAT_OTHER+=("$f"); return ;;
135
+ esac
136
+
137
+ # By filename keywords
138
+ case "$base" in
139
+ layer*|overview*|design*|server-architecture*|*-spec*)
140
+ CAT_ARCH+=("$f"); return ;;
141
+ getting-started*|cli*|grow-your-agent*)
142
+ CAT_START+=("$f"); return ;;
143
+ *) ;;
144
+ esac
145
+
146
+ # Docs directory features
147
+ if [[ "$f" == docs/* ]]; then
148
+ CAT_FEATURES+=("$f")
149
+ else
150
+ CAT_OTHER+=("$f")
151
+ fi
152
+ }
153
+
154
+ for f in "${FILTERED[@]}"; do
155
+ categorise "$f"
156
+ done
157
+
158
+ # --- Output ---
159
+ print_urls() {
160
+ local label="$1"; shift
161
+ local -n arr=$1
162
+
163
+ if [[ ${#arr[@]} -eq 0 ]]; then return; fi
164
+
165
+ if [[ "$PLAIN" == "false" ]]; then
166
+ echo ""
167
+ echo "--- ${label} ---"
168
+ fi
169
+ for f in "${arr[@]}"; do
170
+ echo "${BASE_URL}/${f}"
171
+ done
172
+ }
173
+
174
+ if [[ "$PLAIN" == "false" ]]; then
175
+ echo "=== Documentation Links for NotebookLM ==="
176
+ echo "Repository: ${GITHUB_URL}"
177
+ echo "Branch: ${BRANCH}"
178
+ echo "Files: ${#FILTERED[@]}"
179
+ fi
180
+
181
+ print_urls "Core" CAT_CORE
182
+ print_urls "Architecture" CAT_ARCH
183
+ print_urls "Getting Started" CAT_START
184
+ print_urls "Features" CAT_FEATURES
185
+ print_urls "Protocol" CAT_PROTOCOL
186
+ print_urls "Clients" CAT_CLIENTS
187
+ print_urls "Use Cases" CAT_USECASES
188
+ print_urls "Packages" CAT_PACKAGES
189
+ print_urls "Plans" CAT_PLANS
190
+ print_urls "Specs" CAT_SPECS
191
+ print_urls "Other" CAT_OTHER
192
+
193
+ if [[ "$PLAIN" == "false" ]]; then
194
+ echo ""
195
+ echo "=== Plain URLs (copy-paste block) ==="
196
+ echo ""
197
+ for f in "${FILTERED[@]}"; do
198
+ echo "${BASE_URL}/${f}"
199
+ done
200
+ fi
@@ -0,0 +1,120 @@
1
+ ---
2
+ name: pr-review
3
+ description: >
4
+ Steward-specific PR workflow: branch, commit, push, PR, wait for Qodo/Copilot,
5
+ triage, fix, reply, resolve. Adds a portability lint (no absolute /home paths,
6
+ no per-user dotfile refs in committed docs), an alignment-delta check when
7
+ CLAUDE.md or culture.yaml change, and greenfield-aware test/version-bump
8
+ steps. Use when: creating PRs in steward, handling review feedback, or the
9
+ user says "create PR", "review comments", "address feedback", "resolve threads".
10
+ ---
11
+
12
+ # PR Review — Steward edition
13
+
14
+ Steward's PRs touch agent prompts, `culture.yaml` configs, and cross-project
15
+ guidance. The generic `pr-review` skills don't know that, so they miss two
16
+ classes of bugs Steward keeps producing:
17
+
18
+ - **Path leaks** — committing absolute home-directory paths that work only on
19
+ the author's machine. (PR #1 had four of these.)
20
+ - **Per-user config dependencies** — referencing a dotfile under the user's
21
+ home directory in repo guidance, breaking reproducibility for other
22
+ contributors and CI.
23
+
24
+ This skill specializes Culture's `pr-review` to catch both up front, plus an
25
+ alignment-delta step when Steward-affecting files change. The workflow is
26
+ encapsulated in `scripts/workflow.sh` — follow that, not a manual checklist.
27
+
28
+ ## Prerequisites
29
+
30
+ Hard requirements: `gh` (GitHub CLI), `jq`, `bash`, `python3` (stdlib only),
31
+ `curl` (used by `pr-status.sh`).
32
+
33
+ Soft requirement: `PyYAML` is needed **only for suffix mode** of the sibling
34
+ `agent-config` skill, where it parses Culture's server manifest. Path mode
35
+ and every `pr-review` script work without it. If suffix mode runs without
36
+ PyYAML it exits with a clear install hint.
37
+
38
+ Per-machine paths (sibling-project layout) live in
39
+ `.claude/skills.local.yaml`; see the committed `.example` for the schema.
40
+
41
+ ## How to run
42
+
43
+ `scripts/workflow.sh` is the entry point. Subcommands:
44
+
45
+ | Command | Purpose |
46
+ |---------|---------|
47
+ | `workflow.sh lint` | Portability lint on the current diff (staged + unstaged). |
48
+ | `workflow.sh poll <PR>` | Fetch and display all review comments. |
49
+ | `workflow.sh delta` | Dump each sibling project's `CLAUDE.md` head + `culture.yaml`. |
50
+ | `workflow.sh reply <PR>` | Batch reply (JSONL on stdin) and resolve threads. |
51
+ | `workflow.sh help` | Print this list. |
52
+
53
+ The vendored single-comment helpers — `pr-reply.sh`, `pr-status.sh` — live
54
+ next to `workflow.sh` and are usable directly when batching isn't appropriate.
55
+
56
+ ## End-to-end flow
57
+
58
+ ```text
59
+ git checkout -b <type>/<desc>
60
+ # ... edit ...
61
+ .claude/skills/pr-review/scripts/workflow.sh lint
62
+ git commit -am "..." && git push -u origin <branch>
63
+ gh pr create --title "..." --body "..." # title <70 chars, body signed "- Claude"
64
+ sleep 300 # wait for Qodo + Copilot
65
+ .claude/skills/pr-review/scripts/workflow.sh poll <PR>
66
+ # triage; if CLAUDE.md/culture.yaml/.claude/skills changed:
67
+ .claude/skills/pr-review/scripts/workflow.sh delta
68
+ # fix, re-lint, push
69
+ .claude/skills/pr-review/scripts/workflow.sh reply <PR> < replies.jsonl
70
+ gh pr checks <PR>
71
+ # Wait for human merge — never merge yourself.
72
+ ```
73
+
74
+ Branch naming: `fix/<desc>`, `feat/<desc>`, `docs/<desc>`, `skill/<name>`.
75
+ Commit/PR signature: `- Claude` (workspace convention). The reply script
76
+ auto-appends `- Claude` only if the body isn't already signed, so JSONL
77
+ entries can include or omit it.
78
+
79
+ ## Triage rules
80
+
81
+ For every comment, decide **FIX** or **PUSHBACK** with reasoning.
82
+
83
+ Default to **FIX** for: portability complaints (always valid for Steward —
84
+ recurring bug class), test or doc requests, style nits aligned with workspace
85
+ conventions.
86
+
87
+ Default to **PUSHBACK** for: architecture opinions that conflict with workspace
88
+ `CLAUDE.md` or the all-backends rule; greenfield false-positives (e.g. "add
89
+ tests" before there's any source — defer to a later PR, don't refuse).
90
+
91
+ ### Alignment-delta rule
92
+
93
+ If the PR touches `CLAUDE.md`, `culture.yaml`, or anything under
94
+ `.claude/skills/`, run `workflow.sh delta` **before** declaring FIX or
95
+ PUSHBACK on each comment. The script dumps the head of every sibling
96
+ project's `CLAUDE.md` plus the full `culture.yaml`, using `sibling_projects`
97
+ from `skills.local.yaml`. Note any sibling that needs a follow-up PR and
98
+ mention it in your reply.
99
+
100
+ ## Greenfield-aware steps
101
+
102
+ The lint and the workflow script are always-on. Stack-specific steps are
103
+ conditional and currently no-op (greenfield repo):
104
+
105
+ ```bash
106
+ [ -d tests ] && [ -f pyproject.toml ] && uv run pytest tests/ -x -q
107
+ [ -f pyproject.toml ] && bump_version_per_project_convention # see project README
108
+ [ -f .markdownlint-cli2.yaml ] && markdownlint-cli2 "$(git diff --name-only --cached '*.md')"
109
+ ```
110
+
111
+ Revisit each line as the corresponding stack element actually lands.
112
+
113
+ ## Reply etiquette
114
+
115
+ Every comment must get a reply — no silent fixes. Always pass `--resolve`
116
+ when batch-replying so threads close automatically. Reference the
117
+ review-comment IDs in the fix-up commit message. Steward currently has no
118
+ SonarCloud integration and isn't a registered mesh agent, so skip the
119
+ sonarclaude check and the post-merge IRC ping that Culture's `pr-review`
120
+ includes — those will return when Steward joins those systems.
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env bash
2
+ # Portability lint: catch path leaks and per-user config dependencies in
3
+ # committed docs/configs before they ship in a PR. Steward's recurring bug
4
+ # class.
5
+ #
6
+ # Usage: portability-lint.sh [--all]
7
+ # default: lint files modified vs HEAD (staged + unstaged)
8
+ # --all: lint all tracked files
9
+ #
10
+ # Exits 0 if clean, 1 if any leak is found.
11
+
12
+ set -euo pipefail
13
+
14
+ mode="${1:-diff}"
15
+ case "$mode" in
16
+ --all) files=$(git ls-files -- ':(exclude)*.lock') ;;
17
+ diff|--diff) files=$(git diff --diff-filter=AMR --name-only HEAD -- ':(exclude)*.lock') ;;
18
+ *) echo "Usage: $(basename "$0") [--all]" >&2; exit 2 ;;
19
+ esac
20
+
21
+ [ -z "$files" ] && { echo "(no files to check)"; exit 0; }
22
+
23
+ # ----- Check 1: hard-coded /home/<user>/... paths -----
24
+ hits1=$(echo "$files" | xargs -r grep -nE '/home/[a-z][a-z0-9_-]+/' 2>/dev/null || true)
25
+
26
+ # ----- Check 2: per-user dotfile *config* refs in committed docs/configs -----
27
+ # Carve-outs (allowed, NOT flagged):
28
+ # - ~/.claude/skills/<x>/scripts/ vendored tool calls
29
+ # - ~/.culture/ Culture mesh data this skill is supposed to read
30
+ md_yaml=$(echo "$files" | grep -E '\.(md|ya?ml|toml|json|jsonc)$' || true)
31
+ if [ -n "$md_yaml" ]; then
32
+ hits2=$(echo "$md_yaml" | xargs -r grep -nE '~/\.[A-Za-z]' 2>/dev/null \
33
+ | grep -vE '~/\.claude/skills/[^[:space:]"]+/scripts/' \
34
+ | grep -vE '~/\.culture/' \
35
+ || true)
36
+ else
37
+ hits2=""
38
+ fi
39
+
40
+ fail=0
41
+ if [ -n "$hits1" ]; then
42
+ echo "❌ Hard-coded /home/<user>/ paths:"
43
+ echo "$hits1" | sed 's/^/ /'
44
+ echo " Fix: use ../sibling, repo URL, or \$WORKSPACE/sibling instead."
45
+ fail=1
46
+ fi
47
+ if [ -n "$hits2" ]; then
48
+ [ "$fail" -eq 1 ] && echo
49
+ echo "❌ Per-user ~/.<dotfile> config refs in committed doc/config:"
50
+ echo "$hits2" | sed 's/^/ /'
51
+ echo " Allowed carve-outs: ~/.claude/skills/.../scripts/ (tool calls), ~/.culture/ (mesh data)."
52
+ echo " Otherwise: commit a repo-local config or document a portable lookup."
53
+ fail=1
54
+ fi
55
+
56
+ [ "$fail" -eq 0 ] && echo "✓ portability lint clean ($(echo "$files" | wc -l | tr -d ' ') files checked)"
57
+ exit $fail
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Batch reply to PR review comments from JSONL on stdin.
5
+ # Each line: {"comment_id": 123, "body": "reply text"}
6
+ # Usage: pr-batch.sh [--repo OWNER/REPO] [--resolve] PR_NUMBER < input.jsonl
7
+
8
+ REPO=""
9
+ RESOLVE=false
10
+
11
+ while [[ $# -gt 0 ]]; do
12
+ case "$1" in
13
+ --repo) REPO="$2"; shift 2 ;;
14
+ --resolve) RESOLVE=true; shift ;;
15
+ *) break ;;
16
+ esac
17
+ done
18
+
19
+ PR_NUMBER="${1:?Usage: pr-batch.sh [--repo OWNER/REPO] [--resolve] PR_NUMBER < input.jsonl}"
20
+
21
+ if [[ -z "$REPO" ]]; then
22
+ REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
23
+ fi
24
+
25
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
26
+ RESOLVE_FLAG=""
27
+ if [[ "$RESOLVE" == true ]]; then
28
+ RESOLVE_FLAG="--resolve"
29
+ fi
30
+
31
+ SUCCESS=0
32
+ FAIL=0
33
+
34
+ while IFS= read -r line; do
35
+ # Skip empty lines
36
+ [[ -z "$line" ]] && continue
37
+
38
+ COMMENT_ID=$(echo "$line" | jq -r '.comment_id')
39
+ BODY=$(echo "$line" | jq -r '.body')
40
+
41
+ if [[ "$COMMENT_ID" == "null" || "$BODY" == "null" ]]; then
42
+ echo "SKIP: invalid line: $line"
43
+ ((FAIL++)) || true
44
+ continue
45
+ fi
46
+
47
+ echo "--- Comment $COMMENT_ID ---"
48
+ if bash "$SCRIPT_DIR/pr-reply.sh" --repo "$REPO" $RESOLVE_FLAG "$PR_NUMBER" "$COMMENT_ID" "$BODY"; then
49
+ ((SUCCESS++)) || true
50
+ else
51
+ echo "FAILED: comment $COMMENT_ID"
52
+ ((FAIL++)) || true
53
+ fi
54
+ done
55
+
56
+ echo ""
57
+ echo "Done: $SUCCESS succeeded, $FAIL failed"
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Fetch and display all PR feedback in one pass:
5
+ # 1. Inline review comments (with thread resolve status)
6
+ # 2. Issue comments (qodo summaries, sonarcloud, etc.)
7
+ # 3. Top-level reviews with a non-empty body (copilot overview, etc.)
8
+ #
9
+ # Usage: pr-comments.sh [--repo OWNER/REPO] PR_NUMBER
10
+
11
+ REPO=""
12
+
13
+ while [[ $# -gt 0 ]]; do
14
+ case "$1" in
15
+ --repo) REPO="$2"; shift 2 ;;
16
+ *) break ;;
17
+ esac
18
+ done
19
+
20
+ PR_NUMBER="${1:?Usage: pr-comments.sh [--repo OWNER/REPO] PR_NUMBER}"
21
+
22
+ if [[ -z "$REPO" ]]; then
23
+ REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
24
+ fi
25
+
26
+ # ── Section 1: inline review comments ─────────────────────────────────────
27
+ THREADS_JSON=$(gh api graphql -f query="
28
+ {
29
+ repository(owner: \"${REPO%%/*}\", name: \"${REPO##*/}\") {
30
+ pullRequest(number: $PR_NUMBER) {
31
+ reviewThreads(first: 100) {
32
+ nodes {
33
+ id
34
+ isResolved
35
+ comments(first: 100) {
36
+ nodes { databaseId }
37
+ }
38
+ }
39
+ }
40
+ }
41
+ }
42
+ }" --jq '.data.repository.pullRequest.reviewThreads.nodes')
43
+
44
+ # Build a map from every comment ID in every thread → its thread metadata,
45
+ # so replies in a thread also show resolved status (not just the first comment).
46
+ THREAD_MAP=$(echo "$THREADS_JSON" | jq -r '
47
+ [.[] as $t | $t.comments.nodes[] | {
48
+ comment_id: .databaseId,
49
+ thread_id: $t.id,
50
+ resolved: $t.isResolved
51
+ }]
52
+ ')
53
+
54
+ INLINE=$(gh api "repos/$REPO/pulls/$PR_NUMBER/comments" --paginate)
55
+ INLINE_COUNT=$(echo "$INLINE" | jq 'length')
56
+
57
+ echo "════════════════ INLINE REVIEW COMMENTS ($INLINE_COUNT) ════════════════"
58
+ echo "$INLINE" | jq -r --argjson threads "$THREAD_MAP" '
59
+ .[] | . as $c |
60
+ ($threads | map(select(.comment_id == $c.id)) | first // {resolved: "unknown", thread_id: "?"}) as $t |
61
+ "──────────────────────────────────────────────────",
62
+ "ID: \($c.id) | Thread: \(if $t.resolved == true then "RESOLVED" elif $t.resolved == false then "UNRESOLVED" else "?" end) | Reply-to: \($c.in_reply_to_id // "none")",
63
+ "File: \($c.path):\($c.original_line // $c.line // "?")",
64
+ "Thread ID: \($t.thread_id)",
65
+ "Author: \($c.user.login)",
66
+ "",
67
+ ($c.body | split("\n") | if length > 10 then .[:10] + ["... (truncated)"] else . end | join("\n")),
68
+ ""
69
+ '
70
+
71
+ # ── Section 2: issue comments (general PR comments) ───────────────────────
72
+ ISSUE=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments" --paginate)
73
+ ISSUE_COUNT=$(echo "$ISSUE" | jq 'length')
74
+
75
+ echo ""
76
+ echo "════════════════ ISSUE COMMENTS ($ISSUE_COUNT) ════════════════"
77
+ echo "$ISSUE" | jq -r '
78
+ .[] |
79
+ "──────────────────────────────────────────────────",
80
+ "ID: \(.id) | Author: \(.user.login) | Created: \(.created_at)",
81
+ "",
82
+ (.body | split("\n") | if length > 10 then .[:10] + ["... (truncated)"] else . end | join("\n")),
83
+ ""
84
+ '
85
+
86
+ # ── Section 3: top-level reviews with a body ──────────────────────────────
87
+ REVIEWS=$(gh api "repos/$REPO/pulls/$PR_NUMBER/reviews" --paginate)
88
+ REVIEWS_WITH_BODY=$(echo "$REVIEWS" | jq '[.[] | select((.body // "") != "")]')
89
+ REVIEW_COUNT=$(echo "$REVIEWS_WITH_BODY" | jq 'length')
90
+
91
+ echo ""
92
+ echo "════════════════ TOP-LEVEL REVIEWS ($REVIEW_COUNT) ════════════════"
93
+ echo "$REVIEWS_WITH_BODY" | jq -r '
94
+ .[] |
95
+ "──────────────────────────────────────────────────",
96
+ "Review ID: \(.id) | Author: \(.user.login) | State: \(.state) | Submitted: \(.submitted_at)",
97
+ "",
98
+ (.body | split("\n") | if length > 10 then .[:10] + ["... (truncated)"] else . end | join("\n")),
99
+ ""
100
+ '