vinta-ai-workflows 0.1.7

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 (40) hide show
  1. package/CHANGELOG.md +830 -0
  2. package/LICENSE +21 -0
  3. package/README.md +547 -0
  4. package/package.json +27 -0
  5. package/schemas/README.md +84 -0
  6. package/schemas/mcp-preflight-cache.v1.schema.json +50 -0
  7. package/schemas/prs-context-comments.v1.schema.json +51 -0
  8. package/schemas/prs-context-frontmatter.v1.schema.json +77 -0
  9. package/schemas/sub-agent.v1.schema.json +105 -0
  10. package/schemas/vinta-ai-workflows-config.v1.schema.json +283 -0
  11. package/skills/vinta-analyze-codebase/SKILL.md +325 -0
  12. package/skills/vinta-bootstrap-ai-tools/SKILL.md +375 -0
  13. package/skills/vinta-bootstrap-ai-tools/resources/stacks/README.md +50 -0
  14. package/skills/vinta-bootstrap-ai-tools/resources/stacks/django/notes.md +52 -0
  15. package/skills/vinta-bootstrap-ai-tools/resources/stacks/medplum/notes.md +45 -0
  16. package/skills/vinta-bootstrap-ai-tools/resources/stacks/nextjs-app-router/notes.md +35 -0
  17. package/skills/vinta-bootstrap-ai-tools/resources/stacks/python-package/notes.md +31 -0
  18. package/skills/vinta-bootstrap-ai-tools/resources/stacks/typescript-monorepo/notes.md +32 -0
  19. package/skills/vinta-derive-skills/SKILL.md +200 -0
  20. package/skills/vinta-derive-skills/resources/amend-plan-template.md +355 -0
  21. package/skills/vinta-derive-skills/resources/foundation-skills/add-one-off-script/SKILL.md +292 -0
  22. package/skills/vinta-derive-skills/resources/foundation-skills/add-one-off-script/resources/one_off_script_base.py +602 -0
  23. package/skills/vinta-derive-skills/resources/foundation-skills/add-one-off-script/resources/one_off_script_base.ts +624 -0
  24. package/skills/vinta-derive-skills/resources/foundation-skills/create-qa-use-cases/SKILL.md +232 -0
  25. package/skills/vinta-derive-skills/resources/foundation-skills/create-spec/SKILL.md +235 -0
  26. package/skills/vinta-derive-skills/resources/foundation-skills/open-pr-from-context/SKILL.md +78 -0
  27. package/skills/vinta-derive-skills/resources/foundation-skills/open-pr-from-context/scripts/open-pr.sh +357 -0
  28. package/skills/vinta-derive-skills/resources/foundation-skills/plan-feature/SKILL.md +418 -0
  29. package/skills/vinta-derive-skills/resources/implement-plan-template.md +375 -0
  30. package/skills/vinta-derive-skills/resources/prs-context-template.md +91 -0
  31. package/skills/vinta-derive-skills/resources/systematic-debugging-mcp-tools.md +125 -0
  32. package/skills/vinta-derive-skills/resources/systematic-debugging-template.md +167 -0
  33. package/skills/vinta-derive-subagents/SKILL.md +192 -0
  34. package/skills/vinta-install-ai-tools-setup/SKILL.md +138 -0
  35. package/skills/vinta-install-ai-tools-setup/resources/setup-ai-tools.mjs +392 -0
  36. package/skills/vinta-migrate-plans-specs/SKILL.md +229 -0
  37. package/skills/vinta-sync-ai-tools/SKILL.md +184 -0
  38. package/skills/vinta-update-project-skills/SKILL.md +178 -0
  39. package/skills/vinta-write-agents-md/SKILL.md +192 -0
  40. package/vinta-ai-workflows.mjs +316 -0
@@ -0,0 +1,357 @@
1
+ #!/usr/bin/env bash
2
+ # open-pr.sh — publish one .vinta-ai-workflows/prs-context/{feature}/{phase}.md file as a real PR
3
+ # + inline review comments via the project's PR CLI (`gh` for GitHub,
4
+ # `glab` for GitLab). Reads frontmatter + sections, opens the PR (or
5
+ # detects an existing one), posts each comment from the YAML list,
6
+ # rewrites the file's frontmatter to `status: published` + populated
7
+ # `pr_url`, and appends a publish log.
8
+ #
9
+ # Usage:
10
+ # open-pr.sh <path-to-prs-context-file> [--cli gh|glab] [--dry-run]
11
+ #
12
+ # Exit codes:
13
+ # 0 PR opened (or pre-existing); all comments posted.
14
+ # 1 PR opened; one or more comments failed to post.
15
+ # 2 Hard failure (file invalid, branch not pushed, CLI missing/unauthed,
16
+ # missing dependency).
17
+ #
18
+ # Dependencies:
19
+ # bash 4+, git, yq (Mike Farah's), jq, and one of: gh, glab.
20
+
21
+ set -euo pipefail
22
+
23
+ # ── Output helpers ────────────────────────────────────────────────────────
24
+
25
+ die() { echo "open-pr: $*" >&2; exit 2; }
26
+ log() { echo "open-pr: $*"; }
27
+
28
+ require_cmd() {
29
+ command -v "$1" >/dev/null 2>&1 || die "missing dependency: $1"
30
+ }
31
+
32
+ # ── Args ──────────────────────────────────────────────────────────────────
33
+
34
+ FILE=""
35
+ CLI=""
36
+ DRY_RUN=0
37
+
38
+ while [[ $# -gt 0 ]]; do
39
+ case "$1" in
40
+ --cli) CLI="$2"; shift 2;;
41
+ --cli=*) CLI="${1#--cli=}"; shift;;
42
+ --dry-run|-n) DRY_RUN=1; shift;;
43
+ -h|--help)
44
+ echo "Usage: open-pr.sh <path> [--cli gh|glab] [--dry-run]"
45
+ exit 0
46
+ ;;
47
+ --*) die "unknown arg: $1";;
48
+ *)
49
+ [[ -n "$FILE" ]] && die "unexpected positional arg: $1"
50
+ FILE="$1"; shift
51
+ ;;
52
+ esac
53
+ done
54
+
55
+ [[ -n "$FILE" ]] || die "missing path to prs-context file"
56
+ [[ -f "$FILE" ]] || die "file not found: $FILE"
57
+
58
+ # ── Dep checks (after --help so it works without yq/jq installed) ────────
59
+
60
+ require_cmd git
61
+ require_cmd yq
62
+ require_cmd jq
63
+
64
+ # ── Parse the file ────────────────────────────────────────────────────────
65
+ # Format: --- frontmatter --- \n\n # Title \n ... \n # Description \n ... \n # Comments \n ```yaml ... ```
66
+
67
+ # Frontmatter: lines between the first two `---` markers.
68
+ FRONTMATTER=$(awk '
69
+ /^---$/ { count++; if (count == 2) exit; if (count == 1) next }
70
+ count == 1 { print }
71
+ ' "$FILE")
72
+
73
+ # Body: everything after the second `---` line.
74
+ BODY=$(awk '
75
+ /^---$/ { count++; next }
76
+ count >= 2 { print }
77
+ ' "$FILE")
78
+
79
+ [[ -n "$FRONTMATTER" ]] || die "$FILE: missing YAML frontmatter (--- ... ---)"
80
+
81
+ # Extract typed fields from frontmatter via yq.
82
+ get_meta() { echo "$FRONTMATTER" | yq -r ".${1} // \"\""; }
83
+
84
+ BRANCH=$(get_meta branch)
85
+ BASE=$(get_meta base)
86
+ STATUS=$(get_meta status)
87
+ PR_URL=$(get_meta pr_url)
88
+ PLAN_ID=$(get_meta plan_id)
89
+ PHASE_ID=$(get_meta phase_id)
90
+
91
+ [[ -n "$BRANCH" ]] || die "$FILE: frontmatter missing required 'branch'"
92
+ [[ -n "$BASE" ]] || die "$FILE: frontmatter missing required 'base'"
93
+
94
+ # Already published?
95
+ if [[ "$STATUS" == "published" && -n "$PR_URL" && "$PR_URL" != "null" ]]; then
96
+ log "already published: $PR_URL"
97
+ exit 0
98
+ fi
99
+
100
+ # Body sections — split on `^# ` H1 headings.
101
+ extract_section() {
102
+ local heading="$1"
103
+ echo "$BODY" | awk -v h="^# +${heading}\$" '
104
+ $0 ~ h { in_sec = 1; next }
105
+ /^# / && in_sec { in_sec = 0 }
106
+ in_sec { print }
107
+ ' | sed -e ':a;/./,$!d;/^$/{$d;ba;}' # trim trailing blank lines
108
+ }
109
+
110
+ TITLE=$(extract_section "Title" | sed -e '/./,$!d')
111
+ DESCRIPTION=$(extract_section "Description")
112
+ COMMENTS_RAW=$(extract_section "Comments")
113
+
114
+ [[ -n "$TITLE" ]] || die "$FILE: # Title section is empty"
115
+ [[ -n "$DESCRIPTION" ]] || die "$FILE: # Description section is empty"
116
+
117
+ # Comments are inside ```yaml ... ``` fence within the # Comments section.
118
+ COMMENTS_YAML=$(echo "$COMMENTS_RAW" | awk '
119
+ /^```ya?ml$/ { in_fence = 1; next }
120
+ /^```$/ && in_fence { in_fence = 0; next }
121
+ in_fence { print }
122
+ ')
123
+
124
+ # Convert comments YAML to JSON for jq iteration. Empty list when no fence.
125
+ if [[ -z "$COMMENTS_YAML" ]]; then
126
+ COMMENTS_JSON='[]'
127
+ else
128
+ COMMENTS_JSON=$(echo "$COMMENTS_YAML" | yq -o=json '.' 2>/dev/null || echo '[]')
129
+ [[ "$COMMENTS_JSON" == "null" ]] && COMMENTS_JSON='[]'
130
+ fi
131
+
132
+ COMMENT_COUNT=$(echo "$COMMENTS_JSON" | jq 'length')
133
+
134
+ # ── Detect CLI + verify ──────────────────────────────────────────────────
135
+
136
+ if [[ -z "$CLI" ]]; then
137
+ if command -v gh >/dev/null 2>&1; then CLI=gh
138
+ elif command -v glab >/dev/null 2>&1; then CLI=glab
139
+ else die "no PR CLI found in PATH (gh or glab). install one or pass --cli"
140
+ fi
141
+ fi
142
+
143
+ case "$CLI" in
144
+ gh|glab) ;;
145
+ *) die "unsupported --cli: $CLI";;
146
+ esac
147
+
148
+ require_cmd "$CLI"
149
+ log "using $CLI"
150
+
151
+ if [[ $DRY_RUN -eq 0 ]]; then
152
+ # Auth check.
153
+ "$CLI" auth status >/dev/null 2>&1 || die "$CLI not authenticated. run: $CLI auth login"
154
+
155
+ # Branch pushed?
156
+ git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1 \
157
+ || die "branch not pushed: origin/$BRANCH. push it first."
158
+ fi
159
+
160
+ # ── Open PR (or detect existing) ─────────────────────────────────────────
161
+
162
+ PR_URL_NEW=""
163
+ PR_NUMBER=""
164
+ PR_IS_NEW=0
165
+
166
+ open_pr_gh() {
167
+ # Detect existing PR for branch.
168
+ local existing
169
+ if existing=$(gh pr view "$BRANCH" --json url,number 2>/dev/null); then
170
+ PR_URL_NEW=$(echo "$existing" | jq -r '.url')
171
+ PR_NUMBER=$(echo "$existing" | jq -r '.number')
172
+ PR_IS_NEW=0
173
+ return 0
174
+ fi
175
+ local out
176
+ out=$(gh pr create \
177
+ --base "$BASE" \
178
+ --head "$BRANCH" \
179
+ --title "$TITLE" \
180
+ --body "$DESCRIPTION" 2>&1) \
181
+ || die "gh pr create failed: $out"
182
+ PR_URL_NEW=$(echo "$out" | tail -n1 | tr -d '[:space:]')
183
+ PR_NUMBER=$(echo "$PR_URL_NEW" | grep -oE '/pull/[0-9]+' | grep -oE '[0-9]+')
184
+ PR_IS_NEW=1
185
+ }
186
+
187
+ open_pr_glab() {
188
+ local existing
189
+ if existing=$(glab mr view "$BRANCH" -F json 2>/dev/null); then
190
+ PR_URL_NEW=$(echo "$existing" | jq -r '.web_url')
191
+ PR_NUMBER=$(echo "$existing" | jq -r '.iid')
192
+ PR_IS_NEW=0
193
+ return 0
194
+ fi
195
+ local out
196
+ out=$(glab mr create \
197
+ --target-branch "$BASE" \
198
+ --source-branch "$BRANCH" \
199
+ --title "$TITLE" \
200
+ --description "$DESCRIPTION" \
201
+ --yes 2>&1) \
202
+ || die "glab mr create failed: $out"
203
+ PR_URL_NEW=$(echo "$out" | grep -oE 'https?://[^[:space:]]+' | head -n1)
204
+ PR_NUMBER=$(echo "$PR_URL_NEW" | grep -oE '/[0-9]+$' | tr -d '/')
205
+ PR_IS_NEW=1
206
+ }
207
+
208
+ if [[ $DRY_RUN -eq 1 ]]; then
209
+ PR_URL_NEW="<dry-run>"
210
+ PR_NUMBER="0"
211
+ PR_IS_NEW=1
212
+ else
213
+ case "$CLI" in
214
+ gh) open_pr_gh ;;
215
+ glab) open_pr_glab ;;
216
+ esac
217
+ fi
218
+
219
+ if [[ $PR_IS_NEW -eq 1 ]]; then
220
+ log "PR opened → $PR_URL_NEW"
221
+ else
222
+ log "PR already existed → $PR_URL_NEW"
223
+ fi
224
+
225
+ # ── Post inline comments ─────────────────────────────────────────────────
226
+
227
+ PUBLISH_LOG=()
228
+ NOW() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
229
+
230
+ if [[ $PR_IS_NEW -eq 1 ]]; then
231
+ PUBLISH_LOG+=("$(NOW) — opened: $PR_URL_NEW")
232
+ else
233
+ PUBLISH_LOG+=("$(NOW) — reused existing PR: $PR_URL_NEW")
234
+ fi
235
+
236
+ FAILURES=0
237
+
238
+ post_comment_gh() {
239
+ local file="$1" start="$2" end="$3" side="$4" body="$5"
240
+ local repo commit
241
+ repo=$(gh repo view --json nameWithOwner -q .nameWithOwner)
242
+ commit=$(git rev-parse "origin/$BRANCH")
243
+ local args=(
244
+ api --method POST
245
+ -H "Accept: application/vnd.github+json"
246
+ "/repos/$repo/pulls/$PR_NUMBER/comments"
247
+ -f "body=$body"
248
+ -f "commit_id=$commit"
249
+ -f "path=$file"
250
+ -F "line=${end:-$start}"
251
+ -f "side=${side:-RIGHT}"
252
+ )
253
+ if [[ -n "$end" && "$end" != "$start" && "$end" != "null" ]]; then
254
+ args+=( -F "start_line=$start" -f "start_side=${side:-RIGHT}" )
255
+ fi
256
+ gh "${args[@]}" >/dev/null 2>&1
257
+ }
258
+
259
+ post_comment_glab() {
260
+ local file="$1" start="$2" end="$3" _side="$4" body="$5"
261
+ local refs
262
+ refs=$(glab mr view "$PR_NUMBER" -F json | jq -r '.diff_refs')
263
+ local base_sha head_sha start_sha
264
+ base_sha=$(echo "$refs" | jq -r '.base_sha')
265
+ head_sha=$(echo "$refs" | jq -r '.head_sha')
266
+ start_sha=$(echo "$refs" | jq -r '.start_sha')
267
+ glab api --method POST \
268
+ "projects/:id/merge_requests/$PR_NUMBER/discussions" \
269
+ -f "body=$body" \
270
+ -f "position[base_sha]=$base_sha" \
271
+ -f "position[head_sha]=$head_sha" \
272
+ -f "position[start_sha]=$start_sha" \
273
+ -f "position[position_type]=text" \
274
+ -f "position[new_path]=$file" \
275
+ -f "position[new_line]=${end:-$start}" \
276
+ >/dev/null 2>&1
277
+ }
278
+
279
+ if [[ "$COMMENT_COUNT" -eq 0 ]]; then
280
+ log "no inline comments to post"
281
+ elif [[ $DRY_RUN -eq 1 ]]; then
282
+ log "would post $COMMENT_COUNT comment(s) (dry-run)"
283
+ else
284
+ for ((i = 0; i < COMMENT_COUNT; i++)); do
285
+ file=$(echo "$COMMENTS_JSON" | jq -r ".[$i].file")
286
+ start=$(echo "$COMMENTS_JSON" | jq -r ".[$i].start_line")
287
+ end=$(echo "$COMMENTS_JSON" | jq -r ".[$i].end_line // \"\"")
288
+ side=$(echo "$COMMENTS_JSON" | jq -r ".[$i].side // \"RIGHT\"")
289
+ body=$(echo "$COMMENTS_JSON" | jq -r ".[$i].body")
290
+ label="$file:$start"
291
+ [[ -n "$end" && "$end" != "null" && "$end" != "$start" ]] && label="$label-$end"
292
+
293
+ if [[ "$CLI" == "gh" ]]; then
294
+ if post_comment_gh "$file" "$start" "$end" "$side" "$body"; then
295
+ echo " comment $((i + 1))/$COMMENT_COUNT posted ($label)"
296
+ PUBLISH_LOG+=("$(NOW) — comment $((i + 1))/$COMMENT_COUNT posted ($label)")
297
+ else
298
+ FAILURES=$((FAILURES + 1))
299
+ echo " comment $((i + 1))/$COMMENT_COUNT FAILED ($label)" >&2
300
+ PUBLISH_LOG+=("$(NOW) — comment $((i + 1))/$COMMENT_COUNT FAILED ($label)")
301
+ fi
302
+ else
303
+ if post_comment_glab "$file" "$start" "$end" "$side" "$body"; then
304
+ echo " comment $((i + 1))/$COMMENT_COUNT posted ($label)"
305
+ PUBLISH_LOG+=("$(NOW) — comment $((i + 1))/$COMMENT_COUNT posted ($label)")
306
+ else
307
+ FAILURES=$((FAILURES + 1))
308
+ echo " comment $((i + 1))/$COMMENT_COUNT FAILED ($label)" >&2
309
+ PUBLISH_LOG+=("$(NOW) — comment $((i + 1))/$COMMENT_COUNT FAILED ($label)")
310
+ fi
311
+ fi
312
+ done
313
+ fi
314
+
315
+ # ── Rewrite frontmatter + append publish log ─────────────────────────────
316
+
317
+ if [[ $DRY_RUN -eq 0 ]]; then
318
+ # Update frontmatter status + pr_url.
319
+ NEW_FM=$(echo "$FRONTMATTER" | yq -y \
320
+ ".status = \"published\" | .pr_url = \"$PR_URL_NEW\"" 2>/dev/null \
321
+ || echo "$FRONTMATTER" | yq \
322
+ ".status = \"published\" | .pr_url = \"$PR_URL_NEW\"")
323
+
324
+ # Compose publish log block.
325
+ LOG_BLOCK=$'\n## Publish log\n\n'
326
+ for entry in "${PUBLISH_LOG[@]}"; do
327
+ LOG_BLOCK+="- $entry"$'\n'
328
+ done
329
+
330
+ # If body already has a publish log, append entries to it; else add new section.
331
+ if echo "$BODY" | grep -q '^## Publish log'; then
332
+ NEW_BODY=$(echo "$BODY" | awk -v entries="$(printf '%s\n' "${PUBLISH_LOG[@]}" | sed 's/^/- /')" '
333
+ /^## Publish log/ { in_log = 1; print; next }
334
+ /^## / && in_log { in_log = 0; print entries; print; next }
335
+ { print }
336
+ END { if (in_log) print entries }
337
+ ')
338
+ else
339
+ NEW_BODY="${BODY%$'\n'}"$'\n'"${LOG_BLOCK}"
340
+ fi
341
+
342
+ {
343
+ echo '---'
344
+ echo "$NEW_FM"
345
+ echo '---'
346
+ echo "$NEW_BODY"
347
+ } > "$FILE.tmp" && mv "$FILE.tmp" "$FILE"
348
+
349
+ log "updated $FILE (status=published, pr_url=$PR_URL_NEW)"
350
+ fi
351
+
352
+ if [[ $FAILURES -gt 0 ]]; then
353
+ log "completed with $FAILURES comment failure(s)"
354
+ exit 1
355
+ fi
356
+
357
+ exit 0