agtag 0.1.3__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.
- agtag-0.1.3/.claude/skills/cicd/SKILL.md +164 -0
- agtag-0.1.3/.claude/skills/cicd/scripts/_resolve-nick.sh +43 -0
- agtag-0.1.3/.claude/skills/cicd/scripts/create-pr-and-wait.sh +100 -0
- agtag-0.1.3/.claude/skills/cicd/scripts/poll-readiness.sh +222 -0
- agtag-0.1.3/.claude/skills/cicd/scripts/portability-lint.sh +57 -0
- agtag-0.1.3/.claude/skills/cicd/scripts/pr-batch.sh +57 -0
- agtag-0.1.3/.claude/skills/cicd/scripts/pr-comments.sh +141 -0
- agtag-0.1.3/.claude/skills/cicd/scripts/pr-reply.sh +77 -0
- agtag-0.1.3/.claude/skills/cicd/scripts/pr-status.sh +179 -0
- agtag-0.1.3/.claude/skills/cicd/scripts/wait-and-check.sh +63 -0
- agtag-0.1.3/.claude/skills/cicd/scripts/workflow.sh +211 -0
- agtag-0.1.3/.claude/skills/communicate/SKILL.md +250 -0
- agtag-0.1.3/.claude/skills/communicate/scripts/fetch-issues.sh +57 -0
- agtag-0.1.3/.claude/skills/communicate/scripts/mesh-message.sh +74 -0
- agtag-0.1.3/.claude/skills/communicate/scripts/templates/skill-update-brief.md +99 -0
- agtag-0.1.3/.claude/skills/pypi-maintainer/SKILL.md +75 -0
- agtag-0.1.3/.claude/skills/pypi-maintainer/scripts/switch-source.sh +102 -0
- agtag-0.1.3/.claude/skills/run-tests/SKILL.md +50 -0
- agtag-0.1.3/.claude/skills/run-tests/scripts/test.sh +52 -0
- agtag-0.1.3/.claude/skills/version-bump/SKILL.md +66 -0
- agtag-0.1.3/.claude/skills/version-bump/scripts/bump.py +178 -0
- agtag-0.1.3/.flake8 +3 -0
- agtag-0.1.3/.github/workflows/publish.yml +76 -0
- agtag-0.1.3/.github/workflows/tests.yml +114 -0
- agtag-0.1.3/.gitignore +221 -0
- agtag-0.1.3/.markdownlint-cli2.yaml +23 -0
- agtag-0.1.3/CHANGELOG.md +45 -0
- agtag-0.1.3/CLAUDE.md +32 -0
- agtag-0.1.3/LICENSE +21 -0
- agtag-0.1.3/PKG-INFO +65 -0
- agtag-0.1.3/README.md +47 -0
- agtag-0.1.3/agtag/__init__.py +13 -0
- agtag-0.1.3/agtag/__main__.py +10 -0
- agtag-0.1.3/agtag/cli/__init__.py +126 -0
- agtag-0.1.3/agtag/cli/_commands/__init__.py +0 -0
- agtag-0.1.3/agtag/cli/_commands/explain.py +33 -0
- agtag-0.1.3/agtag/cli/_commands/issue.py +45 -0
- agtag-0.1.3/agtag/cli/_commands/issue_fetch.py +45 -0
- agtag-0.1.3/agtag/cli/_commands/issue_post.py +37 -0
- agtag-0.1.3/agtag/cli/_commands/issue_reply.py +37 -0
- agtag-0.1.3/agtag/cli/_commands/learn.py +87 -0
- agtag-0.1.3/agtag/cli/_errors.py +41 -0
- agtag-0.1.3/agtag/cli/_output.py +53 -0
- agtag-0.1.3/agtag/explain/__init__.py +21 -0
- agtag-0.1.3/agtag/explain/catalog.py +152 -0
- agtag-0.1.3/agtag/issue/__init__.py +0 -0
- agtag-0.1.3/agtag/issue/_gh.py +43 -0
- agtag-0.1.3/agtag/issue/_sign.py +16 -0
- agtag-0.1.3/agtag/issue/fetch.py +54 -0
- agtag-0.1.3/agtag/issue/post.py +34 -0
- agtag-0.1.3/agtag/issue/reply.py +36 -0
- agtag-0.1.3/agtag/nick.py +44 -0
- agtag-0.1.3/culture.yaml +3 -0
- agtag-0.1.3/docs/commands.md +62 -0
- agtag-0.1.3/docs/culture.md +60 -0
- agtag-0.1.3/docs/features.md +50 -0
- agtag-0.1.3/docs/purpose.md +40 -0
- agtag-0.1.3/docs/skill-sources.md +41 -0
- agtag-0.1.3/docs/superpowers/plans/2026-05-09-agtag-cli-scaffold.md +3257 -0
- agtag-0.1.3/docs/superpowers/specs/2026-05-09-agtag-cli-scaffold-design.md +240 -0
- agtag-0.1.3/pyproject.toml +73 -0
- agtag-0.1.3/sonar-project.properties +17 -0
- agtag-0.1.3/tests/__init__.py +0 -0
- agtag-0.1.3/tests/test_cli.py +151 -0
- agtag-0.1.3/tests/test_cmd_handlers.py +305 -0
- agtag-0.1.3/tests/test_errors_output.py +56 -0
- agtag-0.1.3/tests/test_explain_catalog.py +48 -0
- agtag-0.1.3/tests/test_gh.py +40 -0
- agtag-0.1.3/tests/test_issue_fetch.py +41 -0
- agtag-0.1.3/tests/test_issue_post.py +66 -0
- agtag-0.1.3/tests/test_issue_reply.py +63 -0
- agtag-0.1.3/tests/test_nick.py +66 -0
- agtag-0.1.3/tests/test_version.py +10 -0
- agtag-0.1.3/uv.lock +471 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cicd
|
|
3
|
+
description: >
|
|
4
|
+
agtag's CI/CD lane: open PR (auto-wait for Qodo/Copilot), push fixes
|
|
5
|
+
(re-poll bots), triage feedback, reply, resolve. Adds a portability lint
|
|
6
|
+
(no absolute /home paths, no per-user dotfile refs in committed docs),
|
|
7
|
+
an alignment-delta check when CLAUDE.md or culture.yaml change, and
|
|
8
|
+
greenfield-aware test/version-bump steps. Use when: creating PRs in
|
|
9
|
+
agtag, handling review feedback, polling CI status, or the user says
|
|
10
|
+
"create PR", "review comments", "address feedback", "resolve threads".
|
|
11
|
+
Vendored from steward (renamed from `pr-review` in steward 0.7.0).
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# CI/CD — agtag edition (vendored from steward)
|
|
15
|
+
|
|
16
|
+
agtag's PRs touch agent prompts, `culture.yaml` configs, and cross-project
|
|
17
|
+
guidance once they begin landing. The generic `pr-review` skills don't know
|
|
18
|
+
that, so they miss two classes of bugs the AgentCulture stack keeps
|
|
19
|
+
producing in PRs of this shape:
|
|
20
|
+
|
|
21
|
+
- **Path leaks** — committing absolute home-directory paths that work only on
|
|
22
|
+
the author's machine.
|
|
23
|
+
- **Per-user config dependencies** — referencing a dotfile under the user's
|
|
24
|
+
home directory in repo guidance, breaking reproducibility for other
|
|
25
|
+
contributors and CI.
|
|
26
|
+
|
|
27
|
+
This skill specializes Culture's `pr-review` flow to catch both up front, plus
|
|
28
|
+
an alignment-delta step when agtag-affecting files (`CLAUDE.md`,
|
|
29
|
+
`culture.yaml`, vendored skills) change. The workflow is
|
|
30
|
+
encapsulated in `scripts/workflow.sh` — follow that, not a manual checklist.
|
|
31
|
+
|
|
32
|
+
## Prerequisites
|
|
33
|
+
|
|
34
|
+
Hard requirements: `gh` (GitHub CLI), `jq`, `bash`, `python3` (stdlib only),
|
|
35
|
+
`curl` (used by `pr-status.sh`).
|
|
36
|
+
|
|
37
|
+
Soft requirement: `PyYAML` is needed **only for suffix mode** of the sibling
|
|
38
|
+
`agent-config` skill, where it parses Culture's server manifest. Path mode
|
|
39
|
+
and every `cicd` script work without it. If suffix mode runs without
|
|
40
|
+
PyYAML it exits with a clear install hint.
|
|
41
|
+
|
|
42
|
+
Per-machine paths (sibling-project layout) live in
|
|
43
|
+
`.claude/skills.local.yaml`; see the committed `.example` for the schema.
|
|
44
|
+
|
|
45
|
+
## How to run
|
|
46
|
+
|
|
47
|
+
`scripts/workflow.sh` is the entry point. Subcommands:
|
|
48
|
+
|
|
49
|
+
| Command | Purpose |
|
|
50
|
+
|---------|---------|
|
|
51
|
+
| `workflow.sh lint` | Portability lint on the current diff (staged + unstaged). |
|
|
52
|
+
| `workflow.sh open-pr --title T [--body-file F] [--wait SECS] [...]` | `gh pr create` then sleep 180s (or `--wait SECS`) and fetch reviewer comments in one shot. Use after pushing the initial branch. |
|
|
53
|
+
| `workflow.sh poll <PR>` | Fetch and display all review comments. |
|
|
54
|
+
| `workflow.sh poll-readiness <PR> [--max-iters N] [--interval SECS] [--require LIST]` | Loop until all required reviewers are ready (default `qodo`; pass `--require qodo,copilot` to also gate on Copilot) — or the PR closes / iteration cap hits. Headline on stdout, per-iteration diagnostics on stderr. Direct wrapper around `scripts/poll-readiness.sh`. |
|
|
55
|
+
| `workflow.sh wait-after-push <PR> [--wait SECS]` | Sleep 180s (or `--wait SECS`) then re-fetch comments. Use after pushing fixes. |
|
|
56
|
+
| `workflow.sh await <PR>` | Poll for reviewer readiness (default: 30 × 60s ≈ 30 min cap, requires qodo only; tune with `STEWARD_PR_AWAIT_ITERS`, `STEWARD_PR_AWAIT_INTERVAL`, and `STEWARD_PR_REVIEWERS`), then run `pr-status.sh` (CI checks + SonarCloud quality gate, OPEN issues, hotspots) and `pr-comments.sh` (inline / issue / top-level / SonarCloud-new-issues sections). Exits non-zero on SonarCloud `ERROR` or unresolved threads. Setting the legacy `STEWARD_PR_AWAIT_SECONDS=<n>` falls back to a fixed sleep with a deprecation warning. |
|
|
57
|
+
| `workflow.sh delta` | Dump each sibling project's `CLAUDE.md` head + `culture.yaml`. |
|
|
58
|
+
| `workflow.sh reply <PR>` | Batch reply (JSONL on stdin) and resolve threads. |
|
|
59
|
+
| `workflow.sh help` | Print this list. |
|
|
60
|
+
|
|
61
|
+
The vendored single-comment helpers — `pr-reply.sh`, `pr-status.sh` — live
|
|
62
|
+
next to `workflow.sh` and are usable directly when batching isn't appropriate.
|
|
63
|
+
|
|
64
|
+
## Polling for reviewer readiness
|
|
65
|
+
|
|
66
|
+
`scripts/poll-readiness.sh` watches a PR until its required reviewers post
|
|
67
|
+
real (not placeholder) feedback, the PR closes, or an iteration cap fires.
|
|
68
|
+
It fetches `gh api` JSON directly — never `pr-comments.sh` output — so
|
|
69
|
+
truncation can't bias the gate. Default required set is qodo only
|
|
70
|
+
(see header comments and `--help` for tunables, env vars, and the `qodo` /
|
|
71
|
+
`copilot` heuristics; Copilot is detected but not required because its
|
|
72
|
+
review bot is silent on agentculture repos in 2026). Heartbeats stream to
|
|
73
|
+
stderr; the final headline is the only thing on stdout.
|
|
74
|
+
|
|
75
|
+
Two ways to drive it:
|
|
76
|
+
|
|
77
|
+
- **Synchronous** — `workflow.sh await <PR>` after `gh pr create`. The
|
|
78
|
+
main session burns context during the wait; fine up to ~5 minutes.
|
|
79
|
+
- **Asynchronous** — for longer waits, run the looper inside a background
|
|
80
|
+
subagent (Agent tool, `run_in_background: true`) so the main session
|
|
81
|
+
only pays the cache cost when readiness fires. The subagent's only job
|
|
82
|
+
is to invoke `poll-readiness.sh` and echo its headline back. The
|
|
83
|
+
parent triages with `workflow.sh await <PR>` when the notification
|
|
84
|
+
arrives. The user can interrupt with TaskStop.
|
|
85
|
+
|
|
86
|
+
This pattern is borrowed from sibling repo
|
|
87
|
+
[`agentculture/cfafi`](https://github.com/agentculture/cfafi)'s `poll`
|
|
88
|
+
skill — Steward vendors only the looper here rather than promoting `poll`
|
|
89
|
+
to its own first-class skill until other Steward verbs need the same
|
|
90
|
+
primitive.
|
|
91
|
+
|
|
92
|
+
## End-to-end flow
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
git checkout -b <type>/<desc>
|
|
96
|
+
# ... edit ...
|
|
97
|
+
.claude/skills/cicd/scripts/workflow.sh lint
|
|
98
|
+
git commit -am "..." && git push -u origin <branch>
|
|
99
|
+
gh pr create --title "..." --body "..." # title <70 chars, body signed "- <nick> (Claude)"
|
|
100
|
+
.claude/skills/cicd/scripts/workflow.sh await <PR> # readiness loop, then CI + SonarCloud + all comments
|
|
101
|
+
# triage; if CLAUDE.md/culture.yaml/.claude/skills changed:
|
|
102
|
+
.claude/skills/cicd/scripts/workflow.sh delta
|
|
103
|
+
# fix, re-lint, push
|
|
104
|
+
.claude/skills/cicd/scripts/workflow.sh reply <PR> < replies.jsonl
|
|
105
|
+
gh pr checks <PR>
|
|
106
|
+
# Wait for human merge — never merge yourself.
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Branch naming: `fix/<desc>`, `feat/<desc>`, `docs/<desc>`, `skill/<name>`.
|
|
110
|
+
PR / comment signature: `- <nick> (Claude)`, where `<nick>` comes from
|
|
111
|
+
the agent's own `culture.yaml` — first agent's `suffix` — falling back
|
|
112
|
+
to the git-repo basename when no `culture.yaml` is present. The reply
|
|
113
|
+
script resolves this via `scripts/_resolve-nick.sh` and auto-appends the
|
|
114
|
+
signature only when the body isn't already signed, so JSONL reply
|
|
115
|
+
entries can include or omit it. Hand-rolled `gh pr create` and
|
|
116
|
+
`gh issue comment` calls should follow the same convention.
|
|
117
|
+
|
|
118
|
+
## Triage rules
|
|
119
|
+
|
|
120
|
+
For every comment, decide **FIX** or **PUSHBACK** with reasoning.
|
|
121
|
+
|
|
122
|
+
Default to **FIX** for: portability complaints (always valid for AgentCulture
|
|
123
|
+
repos — recurring bug class), test or doc requests, style nits aligned with
|
|
124
|
+
workspace conventions.
|
|
125
|
+
|
|
126
|
+
Default to **PUSHBACK** for: architecture opinions that conflict with workspace
|
|
127
|
+
`CLAUDE.md` or the all-backends rule; greenfield false-positives (e.g. "add
|
|
128
|
+
tests" before there's any source — defer to a later PR, don't refuse).
|
|
129
|
+
|
|
130
|
+
### Alignment-delta rule
|
|
131
|
+
|
|
132
|
+
If the PR touches `CLAUDE.md`, `culture.yaml`, or anything under
|
|
133
|
+
`.claude/skills/`, run `workflow.sh delta` **before** declaring FIX or
|
|
134
|
+
PUSHBACK on each comment. The script dumps the head of every sibling
|
|
135
|
+
project's `CLAUDE.md` plus the full `culture.yaml`, using `sibling_projects`
|
|
136
|
+
from `skills.local.yaml`. Note any sibling that needs a follow-up PR and
|
|
137
|
+
mention it in your reply.
|
|
138
|
+
|
|
139
|
+
## Greenfield-aware steps
|
|
140
|
+
|
|
141
|
+
The lint and the workflow script are always-on. Stack-specific steps are
|
|
142
|
+
conditional and currently no-op (greenfield repo):
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
[ -d tests ] && [ -f pyproject.toml ] && uv run pytest tests/ -x -q
|
|
146
|
+
[ -f pyproject.toml ] && bump_version_per_project_convention # see project README
|
|
147
|
+
[ -f .markdownlint-cli2.yaml ] && markdownlint-cli2 "$(git diff --name-only --cached '*.md')"
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Revisit each line as the corresponding stack element actually lands.
|
|
151
|
+
|
|
152
|
+
## Reply etiquette
|
|
153
|
+
|
|
154
|
+
Every comment must get a reply — no silent fixes. Always pass `--resolve`
|
|
155
|
+
when batch-replying so threads close automatically. Reference the
|
|
156
|
+
review-comment IDs in the fix-up commit message.
|
|
157
|
+
|
|
158
|
+
SonarCloud is queried in two places: `pr-status.sh` (quality gate, OPEN
|
|
159
|
+
issues, hotspots) and the Section-4 dump in `pr-comments.sh` (new-issue
|
|
160
|
+
list). Both derive the project key as `<owner>_<repo>`; override with
|
|
161
|
+
`SONAR_PROJECT_KEY=<key>` for non-standard naming, and they silently skip
|
|
162
|
+
when the project isn't on SonarCloud. agtag isn't yet a registered mesh
|
|
163
|
+
agent, so the post-merge IRC ping that Culture's `pr-review` includes is
|
|
164
|
+
still skipped — that returns when agtag joins the mesh.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Resolve the agent's nick for GitHub message signing.
|
|
5
|
+
# Order: first agent's `suffix` in <repo-root>/culture.yaml,
|
|
6
|
+
# then basename of the git repo root.
|
|
7
|
+
# Prints the nick to stdout. Always exits 0 — pr-reply.sh needs *some*
|
|
8
|
+
# nick to sign with — but if a culture.yaml exists and we couldn't
|
|
9
|
+
# extract a suffix from it, emits a stderr warning so a misconfigured
|
|
10
|
+
# manifest doesn't silently mask itself behind the basename fallback.
|
|
11
|
+
|
|
12
|
+
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
13
|
+
if [[ -z "$repo_root" ]]; then
|
|
14
|
+
repo_root="$PWD"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
manifest="$repo_root/culture.yaml"
|
|
18
|
+
|
|
19
|
+
if [[ -f "$manifest" ]]; then
|
|
20
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
21
|
+
echo "_resolve-nick: python3 not found; cannot parse $manifest, falling back to repo basename" >&2
|
|
22
|
+
else
|
|
23
|
+
nick="$(python3 - "$manifest" <<'PY' 2>/dev/null || true
|
|
24
|
+
import re, sys
|
|
25
|
+
path = sys.argv[1]
|
|
26
|
+
with open(path, encoding="utf-8") as f:
|
|
27
|
+
for raw in f:
|
|
28
|
+
line = raw.rstrip("\n")
|
|
29
|
+
m = re.match(r"^[\s-]*\s*suffix:\s*(\S+)", line)
|
|
30
|
+
if m:
|
|
31
|
+
print(m.group(1).strip("'\""))
|
|
32
|
+
break
|
|
33
|
+
PY
|
|
34
|
+
)"
|
|
35
|
+
if [[ -n "$nick" ]]; then
|
|
36
|
+
printf '%s\n' "$nick"
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
echo "_resolve-nick: $manifest exists but no suffix could be parsed; falling back to repo basename" >&2
|
|
40
|
+
fi
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
basename "$repo_root"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Create a PR, wait for automated reviewers (qodo, copilot, sonarcloud) to
|
|
5
|
+
# post comments, then dump the feedback. Wraps the manual `gh pr create →
|
|
6
|
+
# sleep 300 → pr-comments.sh` sequence into one invocation.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# create-pr-and-wait.sh --title "Title" --body-file PATH [--wait SECS] [extra gh pr create flags...]
|
|
10
|
+
# create-pr-and-wait.sh --title "Title" [--wait SECS] [extra gh pr create flags...] < body-on-stdin
|
|
11
|
+
#
|
|
12
|
+
# Flags:
|
|
13
|
+
# --title TITLE PR title (required)
|
|
14
|
+
# --body-file PATH Read the body from a file. If omitted, reads stdin.
|
|
15
|
+
# --wait SECS How long to sleep after `gh pr create` before fetching
|
|
16
|
+
# comments. Default: 180 (3 min). qodo/copilot/sonarcloud
|
|
17
|
+
# usually post within this window. Use `wait-and-check.sh`
|
|
18
|
+
# to extend by another 3 min if reviewers haven't finished.
|
|
19
|
+
# Any other flags pass through to `gh pr create` (e.g. --base, --reviewer).
|
|
20
|
+
#
|
|
21
|
+
# Behavior:
|
|
22
|
+
# 1. Stage the body to a tempfile and `gh pr create --body-file …` so
|
|
23
|
+
# large self-contained briefs don't hit the OS argv length limit.
|
|
24
|
+
# 2. `sleep $WAIT_SECS` to give reviewers time to post.
|
|
25
|
+
# 3. Run the sibling `pr-comments.sh <PR_NUMBER>` (vendored next to this
|
|
26
|
+
# script) to dump inline + issue + review comments.
|
|
27
|
+
#
|
|
28
|
+
# Exit codes:
|
|
29
|
+
# 0 PR created and feedback fetched (no judgment about whether feedback
|
|
30
|
+
# is clean — caller decides what to do with it).
|
|
31
|
+
# 2 Bad usage (missing --title, or no body source on a TTY).
|
|
32
|
+
# 3 Could not parse PR number from `gh pr create` output.
|
|
33
|
+
# * Whatever `gh pr create` or the comment-fetch step returns.
|
|
34
|
+
|
|
35
|
+
usage() {
|
|
36
|
+
echo "Usage: create-pr-and-wait.sh --title TITLE [--body-file PATH | < stdin] [--wait SECS] [gh pr create flags...]" >&2
|
|
37
|
+
exit 2
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
require_value() {
|
|
41
|
+
if [[ $# -lt 2 ]]; then
|
|
42
|
+
echo "Missing value for $1" >&2
|
|
43
|
+
usage
|
|
44
|
+
fi
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
WAIT_SECS=180
|
|
48
|
+
TITLE=""
|
|
49
|
+
BODY_FILE=""
|
|
50
|
+
PASSTHROUGH=()
|
|
51
|
+
|
|
52
|
+
while [[ $# -gt 0 ]]; do
|
|
53
|
+
case "$1" in
|
|
54
|
+
--title) require_value "$@"; TITLE="$2"; shift 2 ;;
|
|
55
|
+
--body-file) require_value "$@"; BODY_FILE="$2"; shift 2 ;;
|
|
56
|
+
--wait) require_value "$@"; WAIT_SECS="$2"; shift 2 ;;
|
|
57
|
+
-h|--help) usage ;;
|
|
58
|
+
*) PASSTHROUGH+=("$1"); shift ;;
|
|
59
|
+
esac
|
|
60
|
+
done
|
|
61
|
+
|
|
62
|
+
if [[ -z "$TITLE" ]]; then
|
|
63
|
+
usage
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# Stage the body to a tempfile and pass `--body-file` to gh. Large
|
|
67
|
+
# self-contained briefs can otherwise hit the OS argv length limit
|
|
68
|
+
# (~128 KB) when passed via `--body "$BODY"`.
|
|
69
|
+
TMP_BODY=$(mktemp -t cicd-create-pr-body.XXXXXX)
|
|
70
|
+
trap 'rm -f "$TMP_BODY"' EXIT
|
|
71
|
+
|
|
72
|
+
if [[ -n "$BODY_FILE" ]]; then
|
|
73
|
+
cat "$BODY_FILE" > "$TMP_BODY"
|
|
74
|
+
elif [[ ! -t 0 ]]; then
|
|
75
|
+
cat > "$TMP_BODY"
|
|
76
|
+
else
|
|
77
|
+
echo "No --body-file given and stdin is a TTY — refusing to hang on cat." >&2
|
|
78
|
+
echo "Pass --body-file PATH or pipe the body in." >&2
|
|
79
|
+
exit 2
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# Step 1: create the PR
|
|
83
|
+
PR_URL=$(gh pr create --title "$TITLE" --body-file "$TMP_BODY" "${PASSTHROUGH[@]}")
|
|
84
|
+
echo "Created: $PR_URL"
|
|
85
|
+
|
|
86
|
+
PR_NUM=$(echo "$PR_URL" | grep -oE '[0-9]+$' || true)
|
|
87
|
+
if [[ -z "$PR_NUM" ]]; then
|
|
88
|
+
echo "Could not parse PR number from gh pr create output: $PR_URL" >&2
|
|
89
|
+
exit 3
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# Step 2: wait for reviewers
|
|
93
|
+
echo "Waiting ${WAIT_SECS}s for automated reviewers (qodo, copilot, sonarcloud)..."
|
|
94
|
+
sleep "$WAIT_SECS"
|
|
95
|
+
|
|
96
|
+
# Step 3: dump feedback. The cicd skill always vendors pr-comments.sh
|
|
97
|
+
# next to this script — no per-user $HOME fallback (would violate the
|
|
98
|
+
# skills-portability rule).
|
|
99
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
100
|
+
bash "$SCRIPT_DIR/pr-comments.sh" "$PR_NUM"
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# poll-readiness.sh — wait until both automated PR reviewers (qodo + Copilot)
|
|
5
|
+
# have posted their full reviews, OR the PR is merged/closed, OR an iteration
|
|
6
|
+
# cap is hit.
|
|
7
|
+
#
|
|
8
|
+
# Designed to be invoked two ways:
|
|
9
|
+
# (a) Synchronously by `workflow.sh await` (stdin/stdout local to the
|
|
10
|
+
# caller — main session burns context during the wait).
|
|
11
|
+
# (b) Asynchronously by a background subagent (Agent tool with
|
|
12
|
+
# run_in_background:true). The subagent owns the wait so the main
|
|
13
|
+
# session pays the cache cost only once, at completion.
|
|
14
|
+
#
|
|
15
|
+
# Usage:
|
|
16
|
+
# poll-readiness.sh [--repo OWNER/REPO] [--max-iters N] [--interval SECS]
|
|
17
|
+
# [--require LIST] PR_NUMBER
|
|
18
|
+
#
|
|
19
|
+
# Defaults: --max-iters 30, --interval 60, --require qodo (≈30-minute hard cap)
|
|
20
|
+
#
|
|
21
|
+
# --require accepts a comma-separated subset of {qodo,copilot}; the loop
|
|
22
|
+
# exits 0 only when every listed reviewer is "ready" (or PR state flips
|
|
23
|
+
# to MERGED/CLOSED). Override the default via STEWARD_PR_REVIEWERS=...
|
|
24
|
+
# Copilot is *not* required by default — its automated PR-review bot
|
|
25
|
+
# stopped posting top-level reviews on agentculture repos in 2026, so
|
|
26
|
+
# requiring it would cause every wait to TIMEOUT. Re-add `--require
|
|
27
|
+
# qodo,copilot` if Copilot starts posting again.
|
|
28
|
+
#
|
|
29
|
+
# Exit codes:
|
|
30
|
+
# 0 All required reviewers ready, OR PR state is MERGED / CLOSED.
|
|
31
|
+
# 1 TIMEOUT after --max-iters with at least one required reviewer pending.
|
|
32
|
+
# 2 Bad usage.
|
|
33
|
+
#
|
|
34
|
+
# Output:
|
|
35
|
+
# stdout — final headline (≤10 lines), suitable for capture.
|
|
36
|
+
# stderr — per-iteration diagnostics ("still waiting (qodo: …, copilot: …)").
|
|
37
|
+
#
|
|
38
|
+
# Detection heuristics (mirror cfafi's `poll` skill):
|
|
39
|
+
# qodo ready = ISSUE COMMENTS section contains "Code Review by Qodo"
|
|
40
|
+
# AND does NOT contain "Looking for bugs?" (qodo's
|
|
41
|
+
# "still analysing" placeholder).
|
|
42
|
+
# Copilot ready = TOP-LEVEL REVIEWS header reports a count > 0.
|
|
43
|
+
#
|
|
44
|
+
# Dependencies: gh, jq, bash, curl (same as the rest of the skill).
|
|
45
|
+
|
|
46
|
+
usage() {
|
|
47
|
+
cat >&2 <<'EOF'
|
|
48
|
+
Usage: poll-readiness.sh [--repo OWNER/REPO] [--max-iters N] [--interval SECS]
|
|
49
|
+
[--require LIST] PR_NUMBER
|
|
50
|
+
|
|
51
|
+
Defaults: --max-iters 30, --interval 60, --require qodo
|
|
52
|
+
(set STEWARD_PR_REVIEWERS to override the default --require list)
|
|
53
|
+
|
|
54
|
+
--require LIST comma-separated subset of {qodo,copilot}. Exit 0 only
|
|
55
|
+
when every listed reviewer is ready, or PR closes.
|
|
56
|
+
Copilot is not required by default — its bot stopped
|
|
57
|
+
posting top-level reviews on agentculture repos in 2026.
|
|
58
|
+
|
|
59
|
+
Exit 0 when all required reviewers ready (or PR closed),
|
|
60
|
+
exit 1 on TIMEOUT, exit 2 on bad usage.
|
|
61
|
+
EOF
|
|
62
|
+
exit 2
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
require_value() {
|
|
66
|
+
if [[ $# -lt 2 ]]; then
|
|
67
|
+
echo "Missing value for $1" >&2
|
|
68
|
+
usage
|
|
69
|
+
fi
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
REPO=""
|
|
73
|
+
MAX_ITERS=30
|
|
74
|
+
INTERVAL=60
|
|
75
|
+
PR_NUMBER=""
|
|
76
|
+
REQUIRE="${STEWARD_PR_REVIEWERS:-qodo}"
|
|
77
|
+
|
|
78
|
+
while [[ $# -gt 0 ]]; do
|
|
79
|
+
case "$1" in
|
|
80
|
+
--repo) require_value "$@"; REPO="$2"; shift 2 ;;
|
|
81
|
+
--max-iters) require_value "$@"; MAX_ITERS="$2"; shift 2 ;;
|
|
82
|
+
--interval) require_value "$@"; INTERVAL="$2"; shift 2 ;;
|
|
83
|
+
--require) require_value "$@"; REQUIRE="$2"; shift 2 ;;
|
|
84
|
+
-h|--help) usage ;;
|
|
85
|
+
--) shift; break ;;
|
|
86
|
+
-*) echo "Unknown flag: $1" >&2; usage ;;
|
|
87
|
+
*) PR_NUMBER="$1"; shift ;;
|
|
88
|
+
esac
|
|
89
|
+
done
|
|
90
|
+
|
|
91
|
+
[[ -z "$PR_NUMBER" ]] && usage
|
|
92
|
+
[[ "$PR_NUMBER" =~ ^[0-9]+$ ]] || { echo "PR_NUMBER must be a positive integer" >&2; exit 2; }
|
|
93
|
+
[[ "$MAX_ITERS" =~ ^[0-9]+$ ]] || { echo "--max-iters must be a positive integer" >&2; exit 2; }
|
|
94
|
+
[[ "$INTERVAL" =~ ^[0-9]+$ ]] || { echo "--interval must be a positive integer" >&2; exit 2; }
|
|
95
|
+
|
|
96
|
+
# Parse --require into REQUIRE_QODO / REQUIRE_COPILOT booleans. Reject
|
|
97
|
+
# unknown reviewers so a typo (e.g. "qoda") doesn't silently make the
|
|
98
|
+
# loop exit on iteration 1.
|
|
99
|
+
REQUIRE_QODO=0
|
|
100
|
+
REQUIRE_COPILOT=0
|
|
101
|
+
IFS=',' read -ra _REQ <<<"$REQUIRE"
|
|
102
|
+
for r in "${_REQ[@]}"; do
|
|
103
|
+
r="${r// /}"
|
|
104
|
+
case "$r" in
|
|
105
|
+
""|qodo) REQUIRE_QODO=1 ;;
|
|
106
|
+
copilot) REQUIRE_COPILOT=1 ;;
|
|
107
|
+
*) echo "--require: unknown reviewer '$r' (valid: qodo, copilot)" >&2; exit 2 ;;
|
|
108
|
+
esac
|
|
109
|
+
done
|
|
110
|
+
if [[ $REQUIRE_QODO -eq 0 && $REQUIRE_COPILOT -eq 0 ]]; then
|
|
111
|
+
echo "--require: at least one reviewer must be required" >&2
|
|
112
|
+
exit 2
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
if [[ -z "$REPO" ]]; then
|
|
116
|
+
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
120
|
+
PR_URL="https://github.com/${REPO}/pull/${PR_NUMBER}"
|
|
121
|
+
|
|
122
|
+
emit_headline() {
|
|
123
|
+
# $1 final state, $2 qodo status, $3 copilot status, $4 iter count, $5 next-step hint
|
|
124
|
+
cat <<EOF
|
|
125
|
+
PR URL: ${PR_URL}
|
|
126
|
+
Final state: $1
|
|
127
|
+
qodo: $2
|
|
128
|
+
Copilot: $3
|
|
129
|
+
Iterations: $4 / ${MAX_ITERS}
|
|
130
|
+
Next step: $5
|
|
131
|
+
EOF
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
iter=0
|
|
135
|
+
qodo_status="not-posted"
|
|
136
|
+
copilot_status="not-posted"
|
|
137
|
+
|
|
138
|
+
while (( iter < MAX_ITERS )); do
|
|
139
|
+
iter=$((iter + 1))
|
|
140
|
+
|
|
141
|
+
# 1. Short-circuit on closed/merged.
|
|
142
|
+
pr_state=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json state -q .state 2>/dev/null || echo "UNKNOWN")
|
|
143
|
+
if [[ "$pr_state" == "MERGED" || "$pr_state" == "CLOSED" ]]; then
|
|
144
|
+
emit_headline "$pr_state" "$qodo_status" "$copilot_status" "$iter" \
|
|
145
|
+
"PR was ${pr_state,,} before reviewers finished — no triage needed."
|
|
146
|
+
exit 0
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
# 2. Fetch raw, untruncated bodies directly from the GitHub API.
|
|
150
|
+
# Reasoning: pr-comments.sh truncates comment bodies for human display,
|
|
151
|
+
# so its output isn't a reliable input for readiness logic — a long
|
|
152
|
+
# qodo comment can have its "Code Review by Qodo" marker fall past the
|
|
153
|
+
# truncation, and a stale "Looking for bugs?" placeholder elsewhere in
|
|
154
|
+
# the dump can flip a real review back to "placeholder-only". Using
|
|
155
|
+
# `gh api` here also avoids the SonarCloud round-trip pr-comments.sh
|
|
156
|
+
# now does on every iteration (qodo PR #17 review, perf bug).
|
|
157
|
+
issue_json=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments" --paginate 2>/dev/null) || {
|
|
158
|
+
echo "iter ${iter}: gh api issues/comments failed; will retry" >&2
|
|
159
|
+
sleep "$INTERVAL"
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
reviews_json=$(gh api "repos/$REPO/pulls/$PR_NUMBER/reviews" --paginate 2>/dev/null) || {
|
|
163
|
+
echo "iter ${iter}: gh api pulls/reviews failed; will retry" >&2
|
|
164
|
+
sleep "$INTERVAL"
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# 3. qodo readiness — match the structural <h3>Code Review by Qodo</h3>
|
|
169
|
+
# header, which appears only in the done-state body. Cfafi's bare-text
|
|
170
|
+
# heuristic ("contains 'Code Review by Qodo' AND NOT 'Looking for
|
|
171
|
+
# bugs?'") false-positives when qodo's done review *quotes* either
|
|
172
|
+
# string while reporting bugs about polling code (PR #17 hit this).
|
|
173
|
+
# The placeholder comment uses a different layout (no <h3> wrapper),
|
|
174
|
+
# so the H3 marker alone is enough.
|
|
175
|
+
qodo_real=$(echo "$issue_json" | jq '[
|
|
176
|
+
.[] | select(.user.login == "qodo-code-review[bot]")
|
|
177
|
+
| select(.body | contains("<h3>Code Review by Qodo</h3>"))
|
|
178
|
+
] | length')
|
|
179
|
+
qodo_any=$(echo "$issue_json" | jq '[
|
|
180
|
+
.[] | select(.user.login == "qodo-code-review[bot]")
|
|
181
|
+
] | length')
|
|
182
|
+
if (( qodo_real > 0 )); then
|
|
183
|
+
qodo_status="ready"
|
|
184
|
+
elif (( qodo_any > 0 )); then
|
|
185
|
+
qodo_status="placeholder-only"
|
|
186
|
+
else
|
|
187
|
+
qodo_status="not-posted"
|
|
188
|
+
fi
|
|
189
|
+
|
|
190
|
+
# 4. Copilot readiness — at least one top-level review with a non-empty
|
|
191
|
+
# body. Excludes "review whose only content is inline comments".
|
|
192
|
+
copilot_count=$(echo "$reviews_json" | jq '[
|
|
193
|
+
.[] | select((.user.login // "") | startswith("copilot"))
|
|
194
|
+
| select((.body // "") != "")
|
|
195
|
+
] | length')
|
|
196
|
+
if (( copilot_count > 0 )); then
|
|
197
|
+
copilot_status="ready"
|
|
198
|
+
else
|
|
199
|
+
copilot_status="not-posted"
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
# 5. Done?
|
|
203
|
+
qodo_ok=1
|
|
204
|
+
copilot_ok=1
|
|
205
|
+
[[ $REQUIRE_QODO -eq 1 && "$qodo_status" != "ready" ]] && qodo_ok=0
|
|
206
|
+
[[ $REQUIRE_COPILOT -eq 1 && "$copilot_status" != "ready" ]] && copilot_ok=0
|
|
207
|
+
if [[ $qodo_ok -eq 1 && $copilot_ok -eq 1 ]]; then
|
|
208
|
+
emit_headline "OPEN" "$qodo_status" "$copilot_status" "$iter" \
|
|
209
|
+
"Required reviewers ready (require=${REQUIRE}); run pr-status.sh and triage."
|
|
210
|
+
exit 0
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
echo "iter ${iter}/${MAX_ITERS}: qodo=${qodo_status}, copilot=${copilot_status} (require=${REQUIRE}); sleeping ${INTERVAL}s" >&2
|
|
214
|
+
if (( iter < MAX_ITERS )); then
|
|
215
|
+
sleep "$INTERVAL"
|
|
216
|
+
fi
|
|
217
|
+
done
|
|
218
|
+
|
|
219
|
+
# Fell off the loop — TIMEOUT.
|
|
220
|
+
emit_headline "TIMEOUT" "$qodo_status" "$copilot_status" "$MAX_ITERS" \
|
|
221
|
+
"Hit ${MAX_ITERS}-iteration cap; re-run poll-readiness.sh or check PR manually."
|
|
222
|
+
exit 1
|
|
@@ -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"
|