@slamb2k/mad-skills 2.0.40 → 2.0.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/skills/brace/SKILL.md +19 -4
- package/skills/brace/references/claude-md-template.md +14 -0
- package/skills/build/SKILL.md +33 -0
- package/skills/dock/SKILL.md +3 -2
- package/skills/keel/SKILL.md +3 -2
- package/skills/manifest.json +8 -8
- package/skills/rig/SKILL.md +24 -4
- package/skills/ship/SKILL.md +83 -6
- package/skills/ship/references/stage-prompts.md +2 -2
- package/skills/ship/scripts/ci-watch.sh +92 -22
- package/skills/ship/scripts/merge.sh +70 -24
- package/skills/speccy/SKILL.md +19 -0
- package/skills/dock/references/azdo-platform.md +0 -88
- package/skills/keel/references/azdo-platform.md +0 -88
package/package.json
CHANGED
package/skills/brace/SKILL.md
CHANGED
|
@@ -249,6 +249,21 @@ Before sending the prompt, substitute these variables:
|
|
|
249
249
|
|
|
250
250
|
Parse SCAFFOLD_REPORT. If status is "failed", report to user and stop.
|
|
251
251
|
|
|
252
|
+
### Branch Discipline Injection
|
|
253
|
+
|
|
254
|
+
When updating an existing project CLAUDE.md (not creating from template):
|
|
255
|
+
|
|
256
|
+
1. Check if `## Branch Discipline` already exists:
|
|
257
|
+
```bash
|
|
258
|
+
grep -q "## Branch Discipline" CLAUDE.md
|
|
259
|
+
```
|
|
260
|
+
2. If NOT found, inject the Branch Discipline section before `## Guardrails`:
|
|
261
|
+
- Read the file content
|
|
262
|
+
- Find the line containing `## Guardrails`
|
|
263
|
+
- Insert the Branch Discipline section (from the template) immediately before it
|
|
264
|
+
- If no `## Guardrails` section exists, append the section at the end of the file
|
|
265
|
+
3. If already present, skip (idempotent)
|
|
266
|
+
|
|
252
267
|
---
|
|
253
268
|
|
|
254
269
|
## Phase 5: Verification & Report
|
|
@@ -327,12 +342,12 @@ elif echo "$REMOTE_URL" | grep -q 'vs-ssh\.visualstudio\.com'; then
|
|
|
327
342
|
elif echo "$REMOTE_URL" | grep -q 'visualstudio\.com'; then
|
|
328
343
|
AZDO_ORG=$(echo "$REMOTE_URL" | sed -n 's|.*//\([^.]*\)\.visualstudio\.com.*|\1|p')
|
|
329
344
|
AZDO_PROJECT=$(echo "$REMOTE_URL" | sed -n 's|.*/\([^/]*\)/_git/.*|\1|p')
|
|
330
|
-
AZDO_ORG_URL="https
|
|
345
|
+
AZDO_ORG_URL="https://${AZDO_ORG}.visualstudio.com"
|
|
331
346
|
fi
|
|
332
347
|
# URL-decode for CLI/display; keep URL-safe versions for REST API paths
|
|
333
348
|
AZDO_PROJECT_URL_SAFE="$AZDO_PROJECT"
|
|
334
|
-
AZDO_ORG=$(
|
|
335
|
-
AZDO_PROJECT=$(
|
|
349
|
+
AZDO_ORG=$(python3 -c "import urllib.parse; print(urllib.parse.unquote('$AZDO_ORG'))")
|
|
350
|
+
AZDO_PROJECT=$(python3 -c "import urllib.parse; print(urllib.parse.unquote('$AZDO_PROJECT_URL_SAFE'))")
|
|
336
351
|
REPO_NAME=$(basename -s .git "$REMOTE_URL")
|
|
337
352
|
```
|
|
338
353
|
|
|
@@ -351,7 +366,7 @@ If org/project extraction fails, report ⚠️ and skip branch policies.
|
|
|
351
366
|
|
|
352
367
|
**REST fallback:**
|
|
353
368
|
```bash
|
|
354
|
-
AUTH="Authorization: Basic $(
|
|
369
|
+
AUTH="Authorization: Basic $(printf ":%s" "$PAT" | base64 | tr -d '\n')"
|
|
355
370
|
# Get repository ID first
|
|
356
371
|
REPO_ID=$(curl -s -H "$AUTH" \
|
|
357
372
|
"$AZDO_ORG_URL/$AZDO_PROJECT_URL_SAFE/_apis/git/repositories/$REPO_NAME?api-version=7.0" \
|
|
@@ -51,6 +51,20 @@ handles curated facts.
|
|
|
51
51
|
|
|
52
52
|
{UNIVERSAL_PRINCIPLES}
|
|
53
53
|
|
|
54
|
+
## Branch Discipline
|
|
55
|
+
|
|
56
|
+
- **Always sync to main before starting new work** — run `/sync` or
|
|
57
|
+
`git checkout main && git pull` before creating a feature branch
|
|
58
|
+
- **Never branch from a feature branch** — always branch from an up-to-date `main`
|
|
59
|
+
- **One feature per branch** — don't stack unrelated changes on the same branch
|
|
60
|
+
- **After shipping a PR, sync immediately** — checkout main and pull before
|
|
61
|
+
starting the next task
|
|
62
|
+
- **If a PR is pending review**, switch to main before starting unrelated work —
|
|
63
|
+
don't build on top of an unmerged branch
|
|
64
|
+
|
|
65
|
+
These rules prevent divergent branches that require complex rebases with risk
|
|
66
|
+
of silent conflict resolution.
|
|
67
|
+
|
|
54
68
|
## Guardrails
|
|
55
69
|
|
|
56
70
|
- Verify tool output format before chaining into another tool
|
package/skills/build/SKILL.md
CHANGED
|
@@ -141,6 +141,39 @@ Before Stage 1, resolve the PLAN argument into content:
|
|
|
141
141
|
- File: `Plan: {file path} ({line count} lines)`
|
|
142
142
|
- Text: `Plan: inline ({word count} words)`
|
|
143
143
|
|
|
144
|
+
## Pre-Build Branch Check
|
|
145
|
+
|
|
146
|
+
Before starting Stage 1, verify the working tree is suitable for building:
|
|
147
|
+
|
|
148
|
+
1. **Detect current branch and default branch:**
|
|
149
|
+
```bash
|
|
150
|
+
CURRENT=$(git branch --show-current)
|
|
151
|
+
DEFAULT_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@')
|
|
152
|
+
DEFAULT_BRANCH="${DEFAULT_BRANCH:-main}"
|
|
153
|
+
git fetch origin "$DEFAULT_BRANCH" --quiet 2>/dev/null
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
2. **If on a feature branch** (not `main`/`master`/default):
|
|
157
|
+
```bash
|
|
158
|
+
BEHIND=$(git rev-list --count HEAD..origin/"$DEFAULT_BRANCH" 2>/dev/null || echo 0)
|
|
159
|
+
```
|
|
160
|
+
If `BEHIND > 0`, warn the user via `AskUserQuestion`:
|
|
161
|
+
```
|
|
162
|
+
"You're on branch '{CURRENT}' which is {BEHIND} commits behind {DEFAULT_BRANCH}.
|
|
163
|
+
Starting a new feature here risks divergent branches and complex rebases."
|
|
164
|
+
```
|
|
165
|
+
Options:
|
|
166
|
+
- "Switch to main first (Recommended)" — run `/sync`, then create a new branch
|
|
167
|
+
- "Continue on this branch" — proceed (user accepts the risk)
|
|
168
|
+
- "Cancel" — stop
|
|
169
|
+
|
|
170
|
+
3. **If on the default branch** and not up to date:
|
|
171
|
+
```bash
|
|
172
|
+
LOCAL=$(git rev-parse "$DEFAULT_BRANCH")
|
|
173
|
+
REMOTE=$(git rev-parse "origin/$DEFAULT_BRANCH")
|
|
174
|
+
```
|
|
175
|
+
If `LOCAL != REMOTE`, run `/sync` automatically before proceeding.
|
|
176
|
+
|
|
144
177
|
---
|
|
145
178
|
|
|
146
179
|
## Stage 1: Explore
|
package/skills/dock/SKILL.md
CHANGED
|
@@ -128,8 +128,9 @@ For each applicable row, in order:
|
|
|
128
128
|
- **fallback**: notify user with Detail, continue with degraded behavior
|
|
129
129
|
5. After all checks: summarize what's available and what's degraded
|
|
130
130
|
|
|
131
|
-
When `PLATFORM == azdo`, follow
|
|
132
|
-
|
|
131
|
+
When `PLATFORM == azdo`, follow the shared AzDO platform guide
|
|
132
|
+
(repo root: references/azdo-platform.md) for tooling detection (`AZDO_MODE`)
|
|
133
|
+
and configuration validation (`AZDO_ORG`, `AZDO_PROJECT`).
|
|
133
134
|
Pass these variables into all phase prompts alongside `{PLATFORM}`.
|
|
134
135
|
|
|
135
136
|
---
|
package/skills/keel/SKILL.md
CHANGED
|
@@ -124,8 +124,9 @@ For each applicable row, in order:
|
|
|
124
124
|
- **fallback**: notify user with Detail, continue with degraded behavior
|
|
125
125
|
5. After all checks: summarize what's available and what's degraded
|
|
126
126
|
|
|
127
|
-
When `PLATFORM == azdo`, follow
|
|
128
|
-
|
|
127
|
+
When `PLATFORM == azdo`, follow the shared AzDO platform guide
|
|
128
|
+
(repo root: references/azdo-platform.md) for tooling detection (`AZDO_MODE`)
|
|
129
|
+
and configuration validation (`AZDO_ORG`, `AZDO_PROJECT`).
|
|
129
130
|
Pass these variables into all phase prompts alongside `{PLATFORM}`.
|
|
130
131
|
|
|
131
132
|
---
|
package/skills/manifest.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generated": "2026-03-
|
|
2
|
+
"generated": "2026-03-24T07:52:19.071Z",
|
|
3
3
|
"count": 10,
|
|
4
4
|
"skills": [
|
|
5
5
|
{
|
|
6
6
|
"name": "brace",
|
|
7
7
|
"directory": "brace",
|
|
8
8
|
"description": "'Initialize any project directory with a standard scaffold for AI-assisted development. Creates specs/ and context/ directories, a project CLAUDE.md with development workflow and guardrails, .gitignore, and branch protection. Recommends claude-mem for persistent memory. Idempotent — safe to run on existing projects. Triggers: \"init project\", \"setup brace\", \"brace\", \"initialize\", \"bootstrap\", \"scaffold\".'",
|
|
9
|
-
"lines":
|
|
9
|
+
"lines": 460,
|
|
10
10
|
"hasScripts": false,
|
|
11
11
|
"hasReferences": true,
|
|
12
12
|
"hasAssets": true,
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"name": "build",
|
|
17
17
|
"directory": "build",
|
|
18
18
|
"description": "Context-isolated feature development pipeline. Takes a detailed design/plan as argument and executes the full feature-dev lifecycle (explore, question, architect, implement, review, ship) inside subagents so the primary conversation stays compact. Use when you have a well-defined plan and want autonomous execution with minimal context window consumption.",
|
|
19
|
-
"lines":
|
|
19
|
+
"lines": 411,
|
|
20
20
|
"hasScripts": false,
|
|
21
21
|
"hasReferences": true,
|
|
22
22
|
"hasAssets": false,
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"name": "dock",
|
|
37
37
|
"directory": "dock",
|
|
38
38
|
"description": ">- Generate container-based release pipelines that build once and promote immutable artifacts through environments (dev → staging → prod). Detects your stack, interviews for infrastructure choices, then outputs deterministic CI/CD files (Dockerfile, workflows, deployment manifests) that run without an LLM. Use when setting up deployment pipelines, containerizing an app, creating release workflows, or connecting CI to container-friendly infrastructure (Azure Container Apps, AWS Fargate, Google Cloud Run, Kubernetes, Dokku, Coolify, CapRover, etc.).",
|
|
39
|
-
"lines":
|
|
39
|
+
"lines": 432,
|
|
40
40
|
"hasScripts": false,
|
|
41
41
|
"hasReferences": true,
|
|
42
42
|
"hasAssets": false,
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"name": "keel",
|
|
47
47
|
"directory": "keel",
|
|
48
48
|
"description": ">- Generate Infrastructure as Code (IaC) pipelines that provision the cloud and container infrastructure your app deploys to. Interview-driven: detects your stack and cloud provider, then outputs deterministic IaC files (Terraform, Bicep, Pulumi, or CDK) plus CI/CD pipelines that plan on PR and apply on merge. Use when setting up cloud infrastructure, provisioning container registries, databases, networking, DNS, or any infrastructure that containers deploy onto. Designed to run before /dock — /keel lays the infrastructure, /dock deploys to it.",
|
|
49
|
-
"lines":
|
|
49
|
+
"lines": 420,
|
|
50
50
|
"hasScripts": false,
|
|
51
51
|
"hasReferences": true,
|
|
52
52
|
"hasAssets": false,
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"name": "rig",
|
|
67
67
|
"directory": "rig",
|
|
68
68
|
"description": "'Idempotently bootstrap any repository with standard development tools, hooks, and workflows. Use when starting work on a new repo, onboarding to an existing project, or ensuring a repo has proper CI/CD setup. Configures: git hooks (lefthook), commit message templates, PR templates, and GitHub Actions for lint/format/type-check/build. Prompts for user confirmation before changes. Triggers: \"bootstrap repo\", \"setup hooks\", \"configure CI\", \"rig\", \"standardize repo\".'",
|
|
69
|
-
"lines":
|
|
69
|
+
"lines": 363,
|
|
70
70
|
"hasScripts": false,
|
|
71
71
|
"hasReferences": true,
|
|
72
72
|
"hasAssets": true,
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"name": "ship",
|
|
77
77
|
"directory": "ship",
|
|
78
78
|
"description": "\"Ship changes through the full PR lifecycle. Use after completing feature work to commit, push, create PR, wait for checks, and merge. Handles the entire workflow: syncs with main, creates feature branch if needed, groups commits logically with semantic messages, creates detailed PR, monitors CI, fixes issues, squash merges, and cleans up. Invoke when work is ready to ship.\"",
|
|
79
|
-
"lines":
|
|
79
|
+
"lines": 495,
|
|
80
80
|
"hasScripts": true,
|
|
81
81
|
"hasReferences": true,
|
|
82
82
|
"hasAssets": false,
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"name": "speccy",
|
|
87
87
|
"directory": "speccy",
|
|
88
88
|
"description": "Deep-dive interview skill for creating comprehensive specifications. Reviews existing code and docs, then interviews the user through multiple rounds of targeted questions covering technical implementation, UI/UX, concerns, and tradeoffs. Produces a structured spec in specs/. Use when starting a new feature, system, or major change that needs a spec.",
|
|
89
|
-
"lines":
|
|
89
|
+
"lines": 272,
|
|
90
90
|
"hasScripts": false,
|
|
91
91
|
"hasReferences": true,
|
|
92
92
|
"hasAssets": false,
|
package/skills/rig/SKILL.md
CHANGED
|
@@ -166,13 +166,13 @@ elif echo "$REMOTE_URL" | grep -q 'vs-ssh\.visualstudio\.com'; then
|
|
|
166
166
|
elif echo "$REMOTE_URL" | grep -q 'visualstudio\.com'; then
|
|
167
167
|
AZDO_ORG=$(echo "$REMOTE_URL" | sed -n 's|.*//\([^.]*\)\.visualstudio\.com.*|\1|p')
|
|
168
168
|
AZDO_PROJECT=$(echo "$REMOTE_URL" | sed -n 's|.*/\([^/]*\)/_git/.*|\1|p')
|
|
169
|
-
AZDO_ORG_URL="https
|
|
169
|
+
AZDO_ORG_URL="https://${AZDO_ORG}.visualstudio.com"
|
|
170
170
|
fi
|
|
171
171
|
|
|
172
172
|
# URL-decode for CLI/display; keep URL-safe versions for REST API paths
|
|
173
173
|
AZDO_PROJECT_URL_SAFE="$AZDO_PROJECT"
|
|
174
|
-
AZDO_ORG=$(
|
|
175
|
-
AZDO_PROJECT=$(
|
|
174
|
+
AZDO_ORG=$(python3 -c "import urllib.parse; print(urllib.parse.unquote('$AZDO_ORG'))")
|
|
175
|
+
AZDO_PROJECT=$(python3 -c "import urllib.parse; print(urllib.parse.unquote('$AZDO_PROJECT_URL_SAFE'))")
|
|
176
176
|
|
|
177
177
|
if [ -z "$AZDO_ORG" ] || [ -z "$AZDO_PROJECT" ]; then
|
|
178
178
|
echo "❌ Could not extract organization/project from remote URL"
|
|
@@ -193,7 +193,7 @@ az devops configure --defaults organization="$AZDO_ORG_URL" project="$AZDO_PROJE
|
|
|
193
193
|
|
|
194
194
|
When `AZDO_MODE == rest`, store these for API calls:
|
|
195
195
|
- Base URL: `$AZDO_ORG_URL/$AZDO_PROJECT_URL_SAFE/_apis`
|
|
196
|
-
- Auth header: `Authorization: Basic $(
|
|
196
|
+
- Auth header: `Authorization: Basic $(printf ":%s" "$PAT" | base64 | tr -d '\n')`
|
|
197
197
|
|
|
198
198
|
Report in pre-flight:
|
|
199
199
|
- ✅ `azdo context` — org: `{AZDO_ORG}`, project: `{AZDO_PROJECT}`
|
|
@@ -293,6 +293,26 @@ If "Let me choose", present individual options as multi-select.
|
|
|
293
293
|
For each approved item, follow the procedures in
|
|
294
294
|
`references/configuration-steps.md`.
|
|
295
295
|
|
|
296
|
+
### Branch Discipline in CLAUDE.md
|
|
297
|
+
|
|
298
|
+
If the project has an existing `CLAUDE.md`:
|
|
299
|
+
|
|
300
|
+
1. Check if `## Branch Discipline` already exists:
|
|
301
|
+
```bash
|
|
302
|
+
grep -q "## Branch Discipline" CLAUDE.md
|
|
303
|
+
```
|
|
304
|
+
2. If NOT found, inject the Branch Discipline section before `## Guardrails`:
|
|
305
|
+
- Read the file content
|
|
306
|
+
- Find the line containing `## Guardrails`
|
|
307
|
+
- Insert the Branch Discipline section immediately before it
|
|
308
|
+
- If no `## Guardrails` section exists, append at the end of the file
|
|
309
|
+
3. If already present, skip (idempotent)
|
|
310
|
+
|
|
311
|
+
The Branch Discipline content to inject is the `## Branch Discipline` section
|
|
312
|
+
from `skills/brace/references/claude-md-template.md`. Read that file to get
|
|
313
|
+
the exact content — this avoids duplication and ensures both /brace and /rig
|
|
314
|
+
inject identical text.
|
|
315
|
+
|
|
296
316
|
---
|
|
297
317
|
|
|
298
318
|
## Phase 5: Verification
|
package/skills/ship/SKILL.md
CHANGED
|
@@ -183,13 +183,13 @@ elif echo "$REMOTE_URL" | grep -q 'visualstudio\.com'; then
|
|
|
183
183
|
# Legacy HTTPS format: https://{ORG}.visualstudio.com/{PROJECT}/_git/{REPO}
|
|
184
184
|
AZDO_ORG=$(echo "$REMOTE_URL" | sed -n 's|.*//\([^.]*\)\.visualstudio\.com.*|\1|p')
|
|
185
185
|
AZDO_PROJECT=$(echo "$REMOTE_URL" | sed -n 's|.*/\([^/]*\)/_git/.*|\1|p')
|
|
186
|
-
AZDO_ORG_URL="https
|
|
186
|
+
AZDO_ORG_URL="https://${AZDO_ORG}.visualstudio.com"
|
|
187
187
|
fi
|
|
188
188
|
|
|
189
189
|
# URL-decode for CLI/display; keep URL-safe versions for REST API paths
|
|
190
190
|
AZDO_PROJECT_URL_SAFE="$AZDO_PROJECT"
|
|
191
|
-
AZDO_ORG=$(
|
|
192
|
-
AZDO_PROJECT=$(
|
|
191
|
+
AZDO_ORG=$(python3 -c "import urllib.parse; print(urllib.parse.unquote('$AZDO_ORG'))")
|
|
192
|
+
AZDO_PROJECT=$(python3 -c "import urllib.parse; print(urllib.parse.unquote('$AZDO_PROJECT_URL_SAFE'))")
|
|
193
193
|
|
|
194
194
|
if [ -z "$AZDO_ORG" ] || [ -z "$AZDO_PROJECT" ]; then
|
|
195
195
|
echo "❌ Could not extract organization/project from remote URL"
|
|
@@ -210,7 +210,7 @@ az devops configure --defaults organization="$AZDO_ORG_URL" project="$AZDO_PROJE
|
|
|
210
210
|
|
|
211
211
|
When `AZDO_MODE == rest`, store these for API calls:
|
|
212
212
|
- Base URL: `$AZDO_ORG_URL/$AZDO_PROJECT_URL_SAFE/_apis`
|
|
213
|
-
- Auth header: `Authorization: Basic $(
|
|
213
|
+
- Auth header: `Authorization: Basic $(printf ":%s" "$PAT" | base64 | tr -d '\n')`
|
|
214
214
|
|
|
215
215
|
Report in pre-flight:
|
|
216
216
|
- ✅ `azdo context` — org: `{AZDO_ORG}`, project: `{AZDO_PROJECT}`
|
|
@@ -318,16 +318,51 @@ The fix subagent MUST commit and push before returning. Once it returns,
|
|
|
318
318
|
attempt = 0
|
|
319
319
|
while attempt < 2:
|
|
320
320
|
CHECKS = run_watch()
|
|
321
|
-
if CHECKS.status == "all_passed"
|
|
321
|
+
if CHECKS.status == "all_passed":
|
|
322
322
|
break → proceed immediately to Stage 5 (do NOT ask user to confirm merge)
|
|
323
|
+
if CHECKS.status == "no_checks":
|
|
324
|
+
→ prompt user via AskUserQuestion:
|
|
325
|
+
"No CI checks found for PR #{PR_NUMBER} after waiting 30 seconds.
|
|
326
|
+
The repository may not have CI configured, or checks may still be registering."
|
|
327
|
+
Options:
|
|
328
|
+
- "Merge without checks (Recommended)" → break to Stage 5
|
|
329
|
+
- "Wait another 30 seconds" → re-run the watch (do not increment attempt)
|
|
330
|
+
- "Cancel" → stop /ship and display failure banner
|
|
331
|
+
break (if user chose merge or after re-wait resolves)
|
|
323
332
|
attempt += 1
|
|
324
333
|
run_fix(CHECKS.failing_checks)
|
|
325
334
|
→ loop back to watch
|
|
326
335
|
|
|
327
336
|
if attempt == 2 and still failing:
|
|
328
|
-
|
|
337
|
+
→ display failure banner and stop (see Failure Handling below)
|
|
329
338
|
```
|
|
330
339
|
|
|
340
|
+
### Failure Handling
|
|
341
|
+
|
|
342
|
+
When /ship fails at ANY point (CI exhausts fix attempts, merge fails, post-merge
|
|
343
|
+
verification fails), display the failure banner and STOP:
|
|
344
|
+
|
|
345
|
+
```
|
|
346
|
+
┌─ Ship · FAILED ──────────────────────────────────
|
|
347
|
+
│
|
|
348
|
+
│ ❌ PR #{PR_NUMBER} was NOT merged
|
|
349
|
+
│
|
|
350
|
+
│ Reason: {specific failure reason}
|
|
351
|
+
│ Branch: {BRANCH} (still active)
|
|
352
|
+
│
|
|
353
|
+
│ ⚠️ You are still on branch '{BRANCH}'.
|
|
354
|
+
│ Run /sync to return to main before starting new work.
|
|
355
|
+
│
|
|
356
|
+
└───────────────────────────────────────────────────
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**Critical rules on failure:**
|
|
360
|
+
- Do NOT proceed to "What's Next?"
|
|
361
|
+
- Do NOT suggest next tasks or follow-up work
|
|
362
|
+
- Do NOT invoke `/sync` or any other skill
|
|
363
|
+
- Do NOT use language like "will be auto-merged" or "PR is pending"
|
|
364
|
+
- The failure banner is the LAST output — nothing follows it
|
|
365
|
+
|
|
331
366
|
---
|
|
332
367
|
|
|
333
368
|
## Stage 5: Merge & Final Sync
|
|
@@ -352,6 +387,34 @@ bash "$SKILL_ROOT/skills/ship/scripts/merge.sh" \
|
|
|
352
387
|
|
|
353
388
|
Parse LAND_REPORT from output markers. Exit code 0=merged, 1=failed.
|
|
354
389
|
|
|
390
|
+
### 5a-verify. Verify merge completed
|
|
391
|
+
|
|
392
|
+
After merge.sh reports success, verify the PR actually merged:
|
|
393
|
+
|
|
394
|
+
**GitHub:**
|
|
395
|
+
```bash
|
|
396
|
+
PR_STATE=$(gh pr view "$PR_NUMBER" --json state --jq '.state')
|
|
397
|
+
```
|
|
398
|
+
Expected: `"MERGED"`
|
|
399
|
+
|
|
400
|
+
**AzDO CLI:**
|
|
401
|
+
```bash
|
|
402
|
+
PR_STATE=$(az repos pr show --id "$PR_NUMBER" --query status -o tsv)
|
|
403
|
+
```
|
|
404
|
+
Expected: `"completed"`
|
|
405
|
+
|
|
406
|
+
**AzDO REST:**
|
|
407
|
+
```bash
|
|
408
|
+
PR_RESP=$(curl -s -H "$AUTH" "${AZDO_ORG_URL}/${AZDO_PROJECT_URL_SAFE}/_apis/git/repositories/${REPO_ID}/pullRequests/${PR_NUMBER}?api-version=7.1")
|
|
409
|
+
PR_STATE=$(echo "$PR_RESP" | jq -r '.status')
|
|
410
|
+
```
|
|
411
|
+
Expected: `"completed"`
|
|
412
|
+
|
|
413
|
+
If the PR is NOT in the expected merged/completed state, treat as a failure —
|
|
414
|
+
display the failure banner (see Failure Handling) with reason "PR merge was
|
|
415
|
+
accepted but PR is still in '{PR_STATE}' state. The merge may be queued or
|
|
416
|
+
deferred." and stop.
|
|
417
|
+
|
|
355
418
|
### 5b. Sync local repo
|
|
356
419
|
|
|
357
420
|
After the merge script succeeds, run the sync script to checkout the default
|
|
@@ -361,10 +424,23 @@ branch, pull the merge commit, and **clean up stale branches**:
|
|
|
361
424
|
bash "$SKILL_ROOT/skills/sync/scripts/sync.sh" "{REMOTE}" "{DEFAULT_BRANCH}"
|
|
362
425
|
```
|
|
363
426
|
|
|
427
|
+
After sync completes, verify the working tree is on the default branch:
|
|
428
|
+
|
|
429
|
+
```bash
|
|
430
|
+
CURRENT=$(git branch --show-current)
|
|
431
|
+
if [ "$CURRENT" != "{DEFAULT_BRANCH}" ]; then
|
|
432
|
+
git checkout {DEFAULT_BRANCH}
|
|
433
|
+
git pull {REMOTE} {DEFAULT_BRANCH}
|
|
434
|
+
fi
|
|
435
|
+
```
|
|
436
|
+
|
|
364
437
|
---
|
|
365
438
|
|
|
366
439
|
## What's Next?
|
|
367
440
|
|
|
441
|
+
**Only run this section if /ship succeeded (PR is merged).** If any failure
|
|
442
|
+
occurred, the failure banner was already displayed and nothing should follow it.
|
|
443
|
+
|
|
368
444
|
After a successful merge, determine what work comes next by checking these
|
|
369
445
|
sources (in priority order):
|
|
370
446
|
|
|
@@ -393,6 +469,7 @@ Compile all stage reports into a summary:
|
|
|
393
469
|
│ 🌿 Branch: {branch}
|
|
394
470
|
│ 🔗 PR: {pr_url}
|
|
395
471
|
│ 🔀 Merged: {merge_commit} ({merge_type})
|
|
472
|
+
│ 🌿 Now on: {DEFAULT_BRANCH} (up to date)
|
|
396
473
|
│
|
|
397
474
|
│ 📝 Commits
|
|
398
475
|
│ • {commit message 1}
|
|
@@ -130,7 +130,7 @@ Bad examples:
|
|
|
130
130
|
|
|
131
131
|
**If PLATFORM == azdo AND AZDO_MODE == rest:**
|
|
132
132
|
REPO_NAME=$(basename -s .git "$(git remote get-url origin)")
|
|
133
|
-
AUTH="Authorization: Basic $(
|
|
133
|
+
AUTH="Authorization: Basic $(printf ":%s" "{PAT}" | base64 | tr -d '\n')"
|
|
134
134
|
PR_JSON=$(curl -s -X POST \
|
|
135
135
|
-H "$AUTH" \
|
|
136
136
|
-H "Content-Type: application/json" \
|
|
@@ -231,7 +231,7 @@ FAILING CHECKS: {FAILING_CHECKS}
|
|
|
231
231
|
rm -rf "$LOGDIR"
|
|
232
232
|
|
|
233
233
|
**If PLATFORM == azdo AND AZDO_MODE == rest:**
|
|
234
|
-
AUTH="Authorization: Basic $(
|
|
234
|
+
AUTH="Authorization: Basic $(printf ":%s" "{PAT}" | base64 | tr -d '\n')"
|
|
235
235
|
# Get failed build ID
|
|
236
236
|
RUN_ID=$(curl -s -H "$AUTH" \
|
|
237
237
|
"{AZDO_ORG_URL}/{AZDO_PROJECT_URL_SAFE}/_apis/build/builds?branchName=refs/heads/{BRANCH}&resultFilter=failed&\$top=1&api-version=7.0" \
|
|
@@ -22,12 +22,14 @@ for arg in "$@"; do
|
|
|
22
22
|
done
|
|
23
23
|
|
|
24
24
|
STATUS="" CHECKS="" FAILING=""
|
|
25
|
+
GRACE_POLLS=0
|
|
25
26
|
|
|
26
27
|
emit_report() {
|
|
27
28
|
echo "CHECKS_REPORT_BEGIN"
|
|
28
29
|
echo "status=$STATUS"
|
|
29
30
|
echo "checks=$CHECKS"
|
|
30
31
|
echo "failing_checks=${FAILING:-none}"
|
|
32
|
+
echo "grace_period_polls=$GRACE_POLLS"
|
|
31
33
|
echo "CHECKS_REPORT_END"
|
|
32
34
|
}
|
|
33
35
|
|
|
@@ -38,15 +40,29 @@ if [ "$PLATFORM" = "github" ]; then
|
|
|
38
40
|
emit_report; exit 3
|
|
39
41
|
fi
|
|
40
42
|
|
|
43
|
+
# Grace period: wait for checks to register (CI may not trigger immediately)
|
|
44
|
+
GRACE_POLLS=0
|
|
45
|
+
GRACE_JSON="[]"
|
|
46
|
+
for _ in $(seq 1 3); do
|
|
47
|
+
GRACE_POLLS=$((GRACE_POLLS + 1))
|
|
48
|
+
GRACE_JSON=$(gh pr checks "$PR_NUMBER" --json name,state 2>/dev/null || echo "[]")
|
|
49
|
+
if [ "$GRACE_JSON" != "[]" ]; then
|
|
50
|
+
break
|
|
51
|
+
fi
|
|
52
|
+
sleep 10
|
|
53
|
+
done
|
|
54
|
+
|
|
55
|
+
# If no checks found after grace period, report and exit
|
|
56
|
+
if [ "$GRACE_JSON" = "[]" ]; then
|
|
57
|
+
STATUS="no_checks"; CHECKS=""; FAILING="none"
|
|
58
|
+
emit_report; exit 0
|
|
59
|
+
fi
|
|
60
|
+
|
|
41
61
|
# gh pr checks --watch blocks until done; --fail-fast stops on first failure
|
|
42
62
|
gh pr checks "$PR_NUMBER" --watch --fail-fast 2>/dev/null || true
|
|
43
63
|
|
|
44
64
|
# Parse final status
|
|
45
65
|
CHECKS_JSON=$(gh pr checks "$PR_NUMBER" --json name,state 2>/dev/null || echo "[]")
|
|
46
|
-
if [ "$CHECKS_JSON" = "[]" ]; then
|
|
47
|
-
STATUS="no_checks"; CHECKS=""; FAILING="none"
|
|
48
|
-
emit_report; exit 0
|
|
49
|
-
fi
|
|
50
66
|
|
|
51
67
|
FAIL_COUNT=$(echo "$CHECKS_JSON" | jq '[.[] | select(.state=="FAILURE")] | length')
|
|
52
68
|
CHECKS=$(echo "$CHECKS_JSON" | jq -r '.[] | "\(.name):\(.state | ascii_downcase)"' | paste -sd, -)
|
|
@@ -72,19 +88,31 @@ if [ "$AZDO_MODE" = "rest" ]; then
|
|
|
72
88
|
CHECKS="error:no PAT configured"
|
|
73
89
|
emit_report; exit 3
|
|
74
90
|
fi
|
|
75
|
-
AUTH="Authorization: Basic $(
|
|
91
|
+
AUTH="Authorization: Basic $(printf ":%s" "$PAT" | base64 | tr -d '\n')"
|
|
76
92
|
fi
|
|
77
93
|
|
|
78
94
|
# ── AzDO CLI mode ──────────────────────────────────────
|
|
79
95
|
if [ "$AZDO_MODE" = "cli" ]; then
|
|
80
|
-
#
|
|
96
|
+
# Grace period: wait for CI to start (max 2 min)
|
|
97
|
+
# Try PR merge ref first, then fall back to branch name
|
|
81
98
|
RUNS_FOUND=false
|
|
99
|
+
CI_BRANCH=""
|
|
100
|
+
GRACE_POLLS=0
|
|
82
101
|
for _ in $(seq 1 8); do
|
|
102
|
+
GRACE_POLLS=$((GRACE_POLLS + 1))
|
|
103
|
+
# Try PR merge ref first (AzDO sets sourceBranch to refs/pull/<N>/merge)
|
|
104
|
+
RUN_COUNT=$(az pipelines runs list --branch "refs/pull/$PR_NUMBER/merge" --top 5 \
|
|
105
|
+
--org "$AZDO_ORG_URL" --project "$AZDO_PROJECT" \
|
|
106
|
+
--query "length(@)" -o tsv 2>/dev/null)
|
|
107
|
+
if [ -n "$RUN_COUNT" ] && [ "$RUN_COUNT" != "0" ]; then
|
|
108
|
+
RUNS_FOUND=true; CI_BRANCH="refs/pull/$PR_NUMBER/merge"; break
|
|
109
|
+
fi
|
|
110
|
+
# Fallback: try branch name directly
|
|
83
111
|
RUN_COUNT=$(az pipelines runs list --branch "$BRANCH" --top 5 \
|
|
84
112
|
--org "$AZDO_ORG_URL" --project "$AZDO_PROJECT" \
|
|
85
113
|
--query "length(@)" -o tsv 2>/dev/null)
|
|
86
114
|
if [ -n "$RUN_COUNT" ] && [ "$RUN_COUNT" != "0" ]; then
|
|
87
|
-
RUNS_FOUND=true; break
|
|
115
|
+
RUNS_FOUND=true; CI_BRANCH="$BRANCH"; break
|
|
88
116
|
fi
|
|
89
117
|
sleep 15
|
|
90
118
|
done
|
|
@@ -102,14 +130,14 @@ if [ "$AZDO_MODE" = "cli" ]; then
|
|
|
102
130
|
|
|
103
131
|
# Wait for runs to complete with fail-fast (max 30 min)
|
|
104
132
|
for _ in $(seq 1 120); do
|
|
105
|
-
FAILED=$(az pipelines runs list --branch "$
|
|
133
|
+
FAILED=$(az pipelines runs list --branch "$CI_BRANCH" --top 5 \
|
|
106
134
|
--org "$AZDO_ORG_URL" --project "$AZDO_PROJECT" \
|
|
107
135
|
--query "[?result=='failed'] | length(@)" -o tsv 2>/dev/null)
|
|
108
136
|
if [ -n "$FAILED" ] && [ "$FAILED" != "0" ]; then
|
|
109
137
|
break
|
|
110
138
|
fi
|
|
111
139
|
|
|
112
|
-
IN_PROGRESS=$(az pipelines runs list --branch "$
|
|
140
|
+
IN_PROGRESS=$(az pipelines runs list --branch "$CI_BRANCH" --top 5 \
|
|
113
141
|
--org "$AZDO_ORG_URL" --project "$AZDO_PROJECT" \
|
|
114
142
|
--query "[?status=='inProgress'] | length(@)" -o tsv 2>/dev/null)
|
|
115
143
|
if [ "$IN_PROGRESS" = "0" ] || [ -z "$IN_PROGRESS" ]; then break; fi
|
|
@@ -117,7 +145,7 @@ if [ "$AZDO_MODE" = "cli" ]; then
|
|
|
117
145
|
done
|
|
118
146
|
|
|
119
147
|
# Determine final status
|
|
120
|
-
RUNS_TABLE=$(az pipelines runs list --branch "$
|
|
148
|
+
RUNS_TABLE=$(az pipelines runs list --branch "$CI_BRANCH" --top 5 \
|
|
121
149
|
--org "$AZDO_ORG_URL" --project "$AZDO_PROJECT" \
|
|
122
150
|
--query "[].{name:definition.name, result:result}" -o json 2>/dev/null || echo "[]")
|
|
123
151
|
CHECKS=$(echo "$RUNS_TABLE" | jq -r '.[] | "\(.name):\(.result // "pending")"' | paste -sd, -)
|
|
@@ -145,31 +173,64 @@ fi
|
|
|
145
173
|
|
|
146
174
|
# ── AzDO REST mode ─────────────────────────────────────
|
|
147
175
|
if [ "$AZDO_MODE" = "rest" ]; then
|
|
148
|
-
|
|
176
|
+
BUILDS_BASE="$AZDO_ORG_URL/$AZDO_PROJECT_URL_SAFE/_apis/build/builds"
|
|
149
177
|
|
|
150
|
-
#
|
|
178
|
+
# Grace period: wait for CI to start (max 2 min)
|
|
179
|
+
# Try PR merge ref first, then fall back to branch name
|
|
151
180
|
RUNS_FOUND=false
|
|
181
|
+
BUILDS_URL=""
|
|
182
|
+
GRACE_POLLS=0
|
|
152
183
|
for _ in $(seq 1 8); do
|
|
153
|
-
|
|
184
|
+
GRACE_POLLS=$((GRACE_POLLS + 1))
|
|
185
|
+
# Try PR merge ref first (AzDO sets sourceBranch to refs/pull/<N>/merge)
|
|
186
|
+
PR_BUILDS_URL="${BUILDS_BASE}?branchName=refs/pull/$PR_NUMBER/merge&\$top=5&api-version=7.0"
|
|
187
|
+
RESPONSE=$(curl -s -H "$AUTH" "$PR_BUILDS_URL" 2>&1)
|
|
188
|
+
if ! echo "$RESPONSE" | jq empty 2>/dev/null; then
|
|
189
|
+
STATUS="no_checks"; FAILING="none"; CHECKS="error:non-JSON API response"
|
|
190
|
+
emit_report; exit 3
|
|
191
|
+
fi
|
|
192
|
+
RUN_COUNT=$(echo "$RESPONSE" | jq '.value | length')
|
|
193
|
+
if [ -n "$RUN_COUNT" ] && [ "$RUN_COUNT" != "0" ]; then
|
|
194
|
+
RUNS_FOUND=true; BUILDS_URL="$PR_BUILDS_URL"; break
|
|
195
|
+
fi
|
|
196
|
+
# Fallback: try branch name directly
|
|
197
|
+
BRANCH_BUILDS_URL="${BUILDS_BASE}?branchName=refs/heads/$BRANCH&\$top=5&api-version=7.0"
|
|
198
|
+
RESPONSE=$(curl -s -H "$AUTH" "$BRANCH_BUILDS_URL" 2>&1)
|
|
199
|
+
if ! echo "$RESPONSE" | jq empty 2>/dev/null; then
|
|
200
|
+
STATUS="no_checks"; FAILING="none"; CHECKS="error:non-JSON API response"
|
|
201
|
+
emit_report; exit 3
|
|
202
|
+
fi
|
|
203
|
+
RUN_COUNT=$(echo "$RESPONSE" | jq '.value | length')
|
|
154
204
|
if [ -n "$RUN_COUNT" ] && [ "$RUN_COUNT" != "0" ]; then
|
|
155
|
-
RUNS_FOUND=true; break
|
|
205
|
+
RUNS_FOUND=true; BUILDS_URL="$BRANCH_BUILDS_URL"; break
|
|
156
206
|
fi
|
|
157
207
|
sleep 15
|
|
158
208
|
done
|
|
159
209
|
|
|
160
210
|
if [ "$RUNS_FOUND" = false ]; then
|
|
161
|
-
|
|
162
|
-
"$AZDO_ORG_URL/$AZDO_PROJECT_URL_SAFE/_apis/policy/evaluations?artifactId=vstfs:///CodeReview/CodeReviewId/$AZDO_PROJECT_URL_SAFE/$PR_NUMBER&api-version=7.0")
|
|
163
|
-
|
|
211
|
+
RESPONSE=$(curl -s -H "$AUTH" \
|
|
212
|
+
"$AZDO_ORG_URL/$AZDO_PROJECT_URL_SAFE/_apis/policy/evaluations?artifactId=vstfs:///CodeReview/CodeReviewId/$AZDO_PROJECT_URL_SAFE/$PR_NUMBER&api-version=7.0" 2>&1)
|
|
213
|
+
if ! echo "$RESPONSE" | jq empty 2>/dev/null; then
|
|
214
|
+
STATUS="no_checks"; FAILING="none"; CHECKS="error:non-JSON API response"
|
|
215
|
+
emit_report; exit 3
|
|
216
|
+
fi
|
|
217
|
+
EVAL_COUNT=$(echo "$RESPONSE" | jq '.value | length')
|
|
164
218
|
if [ "$EVAL_COUNT" = "0" ] || [ -z "$EVAL_COUNT" ]; then
|
|
165
219
|
STATUS="no_checks"; CHECKS=""; FAILING="none"
|
|
166
220
|
emit_report; exit 0
|
|
167
221
|
fi
|
|
222
|
+
# Default BUILDS_URL for subsequent polling if policies exist but no runs yet
|
|
223
|
+
BUILDS_URL="${BUILDS_BASE}?branchName=refs/pull/$PR_NUMBER/merge&\$top=5&api-version=7.0"
|
|
168
224
|
fi
|
|
169
225
|
|
|
170
226
|
# Wait for runs with fail-fast (max 30 min)
|
|
171
227
|
for _ in $(seq 1 120); do
|
|
172
|
-
|
|
228
|
+
RESPONSE=$(curl -s -H "$AUTH" "$BUILDS_URL" 2>&1)
|
|
229
|
+
if ! echo "$RESPONSE" | jq empty 2>/dev/null; then
|
|
230
|
+
STATUS="no_checks"; FAILING="none"; CHECKS="error:non-JSON API response"
|
|
231
|
+
emit_report; exit 3
|
|
232
|
+
fi
|
|
233
|
+
BUILDS_JSON="$RESPONSE"
|
|
173
234
|
|
|
174
235
|
FAIL_COUNT=$(echo "$BUILDS_JSON" | jq '[.value[] | select(.result=="failed")] | length')
|
|
175
236
|
if [ "${FAIL_COUNT:-0}" -gt 0 ]; then break; fi
|
|
@@ -180,16 +241,25 @@ if [ "$AZDO_MODE" = "rest" ]; then
|
|
|
180
241
|
done
|
|
181
242
|
|
|
182
243
|
# Final status
|
|
183
|
-
|
|
244
|
+
RESPONSE=$(curl -s -H "$AUTH" "$BUILDS_URL" 2>&1)
|
|
245
|
+
if ! echo "$RESPONSE" | jq empty 2>/dev/null; then
|
|
246
|
+
STATUS="no_checks"; FAILING="none"; CHECKS="error:non-JSON API response"
|
|
247
|
+
emit_report; exit 3
|
|
248
|
+
fi
|
|
249
|
+
BUILDS_JSON="$RESPONSE"
|
|
184
250
|
CHECKS=$(echo "$BUILDS_JSON" | jq -r '.value[] | "\(.definition.name):\(.result // "pending")"' | paste -sd, -)
|
|
185
251
|
FAILING=$(echo "$BUILDS_JSON" | jq -r '.value[] | select(.result=="failed") | .definition.name' | paste -sd, -)
|
|
186
252
|
FAIL_COUNT=$(echo "$BUILDS_JSON" | jq '[.value[] | select(.result=="failed")] | length')
|
|
187
253
|
STILL_RUNNING=$(echo "$BUILDS_JSON" | jq '[.value[] | select(.status=="inProgress")] | length')
|
|
188
254
|
|
|
189
255
|
# Check policy evaluations
|
|
190
|
-
|
|
191
|
-
"$AZDO_ORG_URL/$AZDO_PROJECT_URL_SAFE/_apis/policy/evaluations?artifactId=vstfs:///CodeReview/CodeReviewId/$AZDO_PROJECT_URL_SAFE/$PR_NUMBER&api-version=7.0")
|
|
192
|
-
|
|
256
|
+
RESPONSE=$(curl -s -H "$AUTH" \
|
|
257
|
+
"$AZDO_ORG_URL/$AZDO_PROJECT_URL_SAFE/_apis/policy/evaluations?artifactId=vstfs:///CodeReview/CodeReviewId/$AZDO_PROJECT_URL_SAFE/$PR_NUMBER&api-version=7.0" 2>&1)
|
|
258
|
+
if ! echo "$RESPONSE" | jq empty 2>/dev/null; then
|
|
259
|
+
REJECTED="0"
|
|
260
|
+
else
|
|
261
|
+
REJECTED=$(echo "$RESPONSE" | jq '[.value[] | select(.status=="rejected")] | length')
|
|
262
|
+
fi
|
|
193
263
|
|
|
194
264
|
if [ "${FAIL_COUNT:-0}" -gt 0 ] || [ "${REJECTED:-0}" -gt 0 ]; then
|
|
195
265
|
STATUS="some_failed"
|
|
@@ -65,20 +65,38 @@ DELETE_FLAG=$( [ "$DELETE_BRANCH" = true ] && echo "true" || echo "false" )
|
|
|
65
65
|
|
|
66
66
|
# ── AzDO CLI mode ──────────────────────────────────────
|
|
67
67
|
if [ "$AZDO_MODE" = "cli" ]; then
|
|
68
|
-
#
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
--
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
68
|
+
# Wait for all policies to reach terminal state (approved/rejected/notApplicable)
|
|
69
|
+
POLICY_TIMEOUT=20 # 20 iterations × 15 seconds = 5 minutes
|
|
70
|
+
for POLICY_ITER in $(seq 1 $POLICY_TIMEOUT); do
|
|
71
|
+
POLICY_JSON=$(az repos pr policy list --id "$PR_NUMBER" --org "$AZDO_ORG_URL" --project "$AZDO_PROJECT" -o json 2>/dev/null || echo "[]")
|
|
72
|
+
|
|
73
|
+
REJECTED=$(echo "$POLICY_JSON" | jq '[.[] | select(.status=="rejected")] | length')
|
|
74
|
+
PENDING=$(echo "$POLICY_JSON" | jq '[.[] | select(.status=="running" or .status=="queued" or .status=="pending")] | length')
|
|
75
|
+
|
|
76
|
+
if [ "${REJECTED:-0}" -gt 0 ]; then
|
|
77
|
+
STATUS="failed"
|
|
78
|
+
ERRORS="policies rejected"
|
|
79
|
+
MERGE_COMMIT=""; BRANCH_DELETED=false
|
|
80
|
+
emit_report; exit 1
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
if [ "${PENDING:-0}" -eq 0 ]; then
|
|
84
|
+
break # All policies terminal
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
if [ "$POLICY_ITER" -eq "$POLICY_TIMEOUT" ]; then
|
|
88
|
+
STATUS="failed"
|
|
89
|
+
ERRORS="policies not evaluated after 5 minutes"
|
|
90
|
+
MERGE_COMMIT=""; BRANCH_DELETED=false
|
|
91
|
+
emit_report; exit 1
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
sleep 15
|
|
95
|
+
done
|
|
78
96
|
|
|
79
97
|
# Complete the PR
|
|
80
98
|
if az repos pr update --id "$PR_NUMBER" --status completed \
|
|
81
|
-
--org "$AZDO_ORG_URL"
|
|
99
|
+
--org "$AZDO_ORG_URL" \
|
|
82
100
|
--squash "$SQUASH_FLAG" \
|
|
83
101
|
--delete-source-branch "$DELETE_FLAG" 2>/dev/null; then
|
|
84
102
|
STATUS="success"
|
|
@@ -88,7 +106,7 @@ if [ "$AZDO_MODE" = "cli" ]; then
|
|
|
88
106
|
# Retry once after 30s (policies may still be evaluating)
|
|
89
107
|
sleep 30
|
|
90
108
|
if az repos pr update --id "$PR_NUMBER" --status completed \
|
|
91
|
-
--org "$AZDO_ORG_URL"
|
|
109
|
+
--org "$AZDO_ORG_URL" \
|
|
92
110
|
--squash "$SQUASH_FLAG" \
|
|
93
111
|
--delete-source-branch "$DELETE_FLAG" 2>/dev/null; then
|
|
94
112
|
STATUS="success"
|
|
@@ -107,20 +125,42 @@ fi
|
|
|
107
125
|
|
|
108
126
|
# ── AzDO REST mode ─────────────────────────────────────
|
|
109
127
|
if [ "$AZDO_MODE" = "rest" ]; then
|
|
110
|
-
AUTH="Authorization: Basic $(
|
|
128
|
+
AUTH="Authorization: Basic $(printf ":%s" "$PAT" | base64 | tr -d '\n')"
|
|
111
129
|
REPO_NAME=$(basename -s .git "$(git remote get-url origin)")
|
|
112
130
|
PR_API="$AZDO_ORG_URL/$AZDO_PROJECT_URL_SAFE/_apis/git/repositories/$REPO_NAME/pullrequests/$PR_NUMBER"
|
|
113
131
|
|
|
114
|
-
#
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
132
|
+
# Wait for all policy evaluations to reach terminal state
|
|
133
|
+
POLICY_TIMEOUT=20
|
|
134
|
+
for POLICY_ITER in $(seq 1 $POLICY_TIMEOUT); do
|
|
135
|
+
EVALS=$(curl -s -H "$AUTH" \
|
|
136
|
+
"$AZDO_ORG_URL/$AZDO_PROJECT_URL_SAFE/_apis/policy/evaluations?artifactId=vstfs:///CodeReview/CodeReviewId/$AZDO_PROJECT_URL_SAFE/$PR_NUMBER&api-version=7.0" 2>&1)
|
|
137
|
+
if ! echo "$EVALS" | jq empty 2>/dev/null; then
|
|
138
|
+
EVALS='{"value":[]}'
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
REJECTED=$(echo "$EVALS" | jq '[.value[] | select(.status=="rejected")] | length')
|
|
142
|
+
PENDING=$(echo "$EVALS" | jq '[.value[] | select(.status=="running" or .status=="queued" or .status=="pending")] | length')
|
|
143
|
+
|
|
144
|
+
if [ "${REJECTED:-0}" -gt 0 ]; then
|
|
145
|
+
STATUS="failed"
|
|
146
|
+
ERRORS="PR has rejected policy evaluations"
|
|
147
|
+
MERGE_COMMIT=""; BRANCH_DELETED=false
|
|
148
|
+
emit_report; exit 1
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
if [ "${PENDING:-0}" -eq 0 ]; then
|
|
152
|
+
break
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
if [ "$POLICY_ITER" -eq "$POLICY_TIMEOUT" ]; then
|
|
156
|
+
STATUS="failed"
|
|
157
|
+
ERRORS="policies not evaluated after 5 minutes"
|
|
158
|
+
MERGE_COMMIT=""; BRANCH_DELETED=false
|
|
159
|
+
emit_report; exit 1
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
sleep 15
|
|
163
|
+
done
|
|
124
164
|
|
|
125
165
|
# Resolve merge strategy
|
|
126
166
|
MERGE_STRATEGY=$( [ "$SQUASH" = true ] && echo "squash" || echo "noFastForward" )
|
|
@@ -128,7 +168,13 @@ if [ "$AZDO_MODE" = "rest" ]; then
|
|
|
128
168
|
# Complete the PR
|
|
129
169
|
RESPONSE=$(curl -s -X PATCH -H "$AUTH" -H "Content-Type: application/json" \
|
|
130
170
|
"$PR_API?api-version=7.0" \
|
|
131
|
-
-d "{\"status\": \"completed\", \"completionOptions\": {\"mergeStrategy\": \"$MERGE_STRATEGY\", \"deleteSourceBranch\": $DELETE_FLAG}}")
|
|
171
|
+
-d "{\"status\": \"completed\", \"completionOptions\": {\"mergeStrategy\": \"$MERGE_STRATEGY\", \"deleteSourceBranch\": $DELETE_FLAG}}" 2>&1)
|
|
172
|
+
if ! echo "$RESPONSE" | jq empty 2>/dev/null; then
|
|
173
|
+
STATUS="failed"
|
|
174
|
+
ERRORS="REST merge returned non-JSON response"
|
|
175
|
+
MERGE_COMMIT=""; BRANCH_DELETED=false
|
|
176
|
+
emit_report; exit 1
|
|
177
|
+
fi
|
|
132
178
|
|
|
133
179
|
PR_STATUS=$(echo "$RESPONSE" | jq -r '.status // empty')
|
|
134
180
|
if [ "$PR_STATUS" = "completed" ]; then
|
package/skills/speccy/SKILL.md
CHANGED
|
@@ -84,6 +84,25 @@ For each row, in order:
|
|
|
84
84
|
|
|
85
85
|
## Stage 1: Context Gathering
|
|
86
86
|
|
|
87
|
+
### Pre-Spec Branch Check
|
|
88
|
+
|
|
89
|
+
Before gathering context, check if the user is on a stale branch:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
CURRENT=$(git branch --show-current)
|
|
93
|
+
if [ "$CURRENT" != "main" ] && [ "$CURRENT" != "master" ]; then
|
|
94
|
+
git fetch origin main --quiet 2>/dev/null
|
|
95
|
+
BEHIND=$(git rev-list --count HEAD..origin/main 2>/dev/null || echo 0)
|
|
96
|
+
if [ "$BEHIND" -gt 5 ]; then
|
|
97
|
+
echo "⚠️ Branch '$CURRENT' is $BEHIND commits behind main."
|
|
98
|
+
echo " Consider running /sync before building from this spec."
|
|
99
|
+
fi
|
|
100
|
+
fi
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
This is advisory only (specs don't modify code) — do not block, continue
|
|
104
|
+
regardless of the result.
|
|
105
|
+
|
|
87
106
|
Before asking any questions, build a thorough understanding of the project:
|
|
88
107
|
|
|
89
108
|
1. **Capture GOAL** — the user's argument describing what needs to be specified
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
# Azure DevOps Platform Support
|
|
2
|
-
|
|
3
|
-
Shared procedures for AzDO tooling detection and configuration validation.
|
|
4
|
-
Referenced by SKILL.md during pre-flight when `PLATFORM == azdo`.
|
|
5
|
-
|
|
6
|
-
## AzDO Tooling Detection
|
|
7
|
-
|
|
8
|
-
When `PLATFORM == azdo`, determine which tooling is available. Set `AZDO_MODE`
|
|
9
|
-
for use in all subsequent phases:
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
if az devops -h &>/dev/null; then
|
|
13
|
-
AZDO_MODE="cli"
|
|
14
|
-
else
|
|
15
|
-
AZDO_MODE="rest"
|
|
16
|
-
fi
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
- **`cli`**: Use `az repos` / `az pipelines` commands (preferred)
|
|
20
|
-
- **`rest`**: Use Azure DevOps REST API via `curl`. Requires a PAT (personal
|
|
21
|
-
access token) in `AZURE_DEVOPS_EXT_PAT` or `AZDO_PAT` env var. If no PAT
|
|
22
|
-
is found, prompt the user to either install the CLI or set the env var.
|
|
23
|
-
|
|
24
|
-
Report in pre-flight:
|
|
25
|
-
- ✅ `az devops cli` — version found
|
|
26
|
-
- ⚠️ `az devops cli` — not found → using REST API fallback
|
|
27
|
-
- ❌ `az devops cli` — not found, no PAT configured → halt with setup instructions
|
|
28
|
-
|
|
29
|
-
## AzDO Configuration Validation
|
|
30
|
-
|
|
31
|
-
When `PLATFORM == azdo`, extract organization and project from the remote URL
|
|
32
|
-
and validate they are usable. These values are needed by every `az repos` /
|
|
33
|
-
`az pipelines` command and every REST API call.
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
# Extract org and project from remote URL patterns:
|
|
37
|
-
# https://dev.azure.com/{ORG}/{PROJECT}/_git/{REPO}
|
|
38
|
-
# https://{ORG}@dev.azure.com/{ORG}/{PROJECT}/_git/{REPO}
|
|
39
|
-
# {ORG}@vs-ssh.visualstudio.com:v3/{ORG}/{PROJECT}/{REPO}
|
|
40
|
-
|
|
41
|
-
REMOTE_URL=$(git remote get-url origin 2>/dev/null)
|
|
42
|
-
|
|
43
|
-
if echo "$REMOTE_URL" | grep -q 'dev\.azure\.com'; then
|
|
44
|
-
AZDO_ORG=$(echo "$REMOTE_URL" | sed -n 's|.*dev\.azure\.com/\([^/]*\)/.*|\1|p')
|
|
45
|
-
AZDO_PROJECT=$(echo "$REMOTE_URL" | sed -n 's|.*dev\.azure\.com/[^/]*/\([^/]*\)/.*|\1|p')
|
|
46
|
-
AZDO_ORG_URL="https://dev.azure.com/$AZDO_ORG"
|
|
47
|
-
elif echo "$REMOTE_URL" | grep -q 'vs-ssh\.visualstudio\.com'; then
|
|
48
|
-
AZDO_ORG=$(echo "$REMOTE_URL" | sed -n 's|.*vs-ssh\.visualstudio\.com:v3/\([^/]*\)/.*|\1|p')
|
|
49
|
-
AZDO_PROJECT=$(echo "$REMOTE_URL" | sed -n 's|.*vs-ssh\.visualstudio\.com:v3/[^/]*/\([^/]*\)/.*|\1|p')
|
|
50
|
-
AZDO_ORG_URL="https://dev.azure.com/$AZDO_ORG"
|
|
51
|
-
elif echo "$REMOTE_URL" | grep -q 'visualstudio\.com'; then
|
|
52
|
-
AZDO_ORG=$(echo "$REMOTE_URL" | sed -n 's|.*//\([^.]*\)\.visualstudio\.com.*|\1|p')
|
|
53
|
-
AZDO_PROJECT=$(echo "$REMOTE_URL" | sed -n 's|.*/\([^/]*\)/_git/.*|\1|p')
|
|
54
|
-
AZDO_ORG_URL="https://dev.azure.com/$AZDO_ORG"
|
|
55
|
-
fi
|
|
56
|
-
|
|
57
|
-
# URL-decode for CLI/display; keep URL-safe versions for REST API paths
|
|
58
|
-
AZDO_PROJECT_URL_SAFE="$AZDO_PROJECT"
|
|
59
|
-
AZDO_ORG=$(printf '%b' "${AZDO_ORG//%/\\x}")
|
|
60
|
-
AZDO_PROJECT=$(printf '%b' "${AZDO_PROJECT//%/\\x}")
|
|
61
|
-
|
|
62
|
-
if [ -z "$AZDO_ORG" ] || [ -z "$AZDO_PROJECT" ]; then
|
|
63
|
-
echo "❌ Could not extract organization/project from remote URL"
|
|
64
|
-
echo " Remote: $REMOTE_URL"
|
|
65
|
-
echo ""
|
|
66
|
-
echo "Ensure the remote URL follows one of these formats:"
|
|
67
|
-
echo " https://dev.azure.com/{ORG}/{PROJECT}/_git/{REPO}"
|
|
68
|
-
echo " https://{ORG}.visualstudio.com/{PROJECT}/_git/{REPO}"
|
|
69
|
-
echo " {ORG}@vs-ssh.visualstudio.com:v3/{ORG}/{PROJECT}/{REPO}"
|
|
70
|
-
# HALT — cannot proceed without org/project context
|
|
71
|
-
fi
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
When `AZDO_MODE == cli`, also configure the defaults so commands work correctly:
|
|
75
|
-
```bash
|
|
76
|
-
az devops configure --defaults organization="$AZDO_ORG_URL" project="$AZDO_PROJECT"
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
When `AZDO_MODE == rest`, store these for API calls:
|
|
80
|
-
- Base URL: `$AZDO_ORG_URL/$AZDO_PROJECT_URL_SAFE/_apis`
|
|
81
|
-
- Auth header: `Authorization: Basic $(echo -n ":$PAT" | base64)`
|
|
82
|
-
|
|
83
|
-
Report in pre-flight:
|
|
84
|
-
- ✅ `azdo context` — org: `{AZDO_ORG}`, project: `{AZDO_PROJECT}`
|
|
85
|
-
- ❌ `azdo context` — could not parse from remote URL → halt with instructions
|
|
86
|
-
|
|
87
|
-
Pass `{AZDO_MODE}`, `{AZDO_ORG}`, `{AZDO_PROJECT}`, `{AZDO_ORG_URL}` into
|
|
88
|
-
all phase prompts alongside `{PLATFORM}`.
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
# Azure DevOps Platform Support
|
|
2
|
-
|
|
3
|
-
Shared procedures for AzDO tooling detection and configuration validation.
|
|
4
|
-
Referenced by SKILL.md during pre-flight when `PLATFORM == azdo`.
|
|
5
|
-
|
|
6
|
-
## AzDO Tooling Detection
|
|
7
|
-
|
|
8
|
-
When `PLATFORM == azdo`, determine which tooling is available. Set `AZDO_MODE`
|
|
9
|
-
for use in all subsequent phases:
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
if az devops -h &>/dev/null; then
|
|
13
|
-
AZDO_MODE="cli"
|
|
14
|
-
else
|
|
15
|
-
AZDO_MODE="rest"
|
|
16
|
-
fi
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
- **`cli`**: Use `az repos` / `az pipelines` commands (preferred)
|
|
20
|
-
- **`rest`**: Use Azure DevOps REST API via `curl`. Requires a PAT (personal
|
|
21
|
-
access token) in `AZURE_DEVOPS_EXT_PAT` or `AZDO_PAT` env var. If no PAT
|
|
22
|
-
is found, prompt the user to either install the CLI or set the env var.
|
|
23
|
-
|
|
24
|
-
Report in pre-flight:
|
|
25
|
-
- ✅ `az devops cli` — version found
|
|
26
|
-
- ⚠️ `az devops cli` — not found → using REST API fallback
|
|
27
|
-
- ❌ `az devops cli` — not found, no PAT configured → halt with setup instructions
|
|
28
|
-
|
|
29
|
-
## AzDO Configuration Validation
|
|
30
|
-
|
|
31
|
-
When `PLATFORM == azdo`, extract organization and project from the remote URL
|
|
32
|
-
and validate they are usable. These values are needed by every `az repos` /
|
|
33
|
-
`az pipelines` command and every REST API call.
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
# Extract org and project from remote URL patterns:
|
|
37
|
-
# https://dev.azure.com/{ORG}/{PROJECT}/_git/{REPO}
|
|
38
|
-
# https://{ORG}@dev.azure.com/{ORG}/{PROJECT}/_git/{REPO}
|
|
39
|
-
# {ORG}@vs-ssh.visualstudio.com:v3/{ORG}/{PROJECT}/{REPO}
|
|
40
|
-
|
|
41
|
-
REMOTE_URL=$(git remote get-url origin 2>/dev/null)
|
|
42
|
-
|
|
43
|
-
if echo "$REMOTE_URL" | grep -q 'dev\.azure\.com'; then
|
|
44
|
-
AZDO_ORG=$(echo "$REMOTE_URL" | sed -n 's|.*dev\.azure\.com/\([^/]*\)/.*|\1|p')
|
|
45
|
-
AZDO_PROJECT=$(echo "$REMOTE_URL" | sed -n 's|.*dev\.azure\.com/[^/]*/\([^/]*\)/.*|\1|p')
|
|
46
|
-
AZDO_ORG_URL="https://dev.azure.com/$AZDO_ORG"
|
|
47
|
-
elif echo "$REMOTE_URL" | grep -q 'vs-ssh\.visualstudio\.com'; then
|
|
48
|
-
AZDO_ORG=$(echo "$REMOTE_URL" | sed -n 's|.*vs-ssh\.visualstudio\.com:v3/\([^/]*\)/.*|\1|p')
|
|
49
|
-
AZDO_PROJECT=$(echo "$REMOTE_URL" | sed -n 's|.*vs-ssh\.visualstudio\.com:v3/[^/]*/\([^/]*\)/.*|\1|p')
|
|
50
|
-
AZDO_ORG_URL="https://dev.azure.com/$AZDO_ORG"
|
|
51
|
-
elif echo "$REMOTE_URL" | grep -q 'visualstudio\.com'; then
|
|
52
|
-
AZDO_ORG=$(echo "$REMOTE_URL" | sed -n 's|.*//\([^.]*\)\.visualstudio\.com.*|\1|p')
|
|
53
|
-
AZDO_PROJECT=$(echo "$REMOTE_URL" | sed -n 's|.*/\([^/]*\)/_git/.*|\1|p')
|
|
54
|
-
AZDO_ORG_URL="https://dev.azure.com/$AZDO_ORG"
|
|
55
|
-
fi
|
|
56
|
-
|
|
57
|
-
# URL-decode for CLI/display; keep URL-safe versions for REST API paths
|
|
58
|
-
AZDO_PROJECT_URL_SAFE="$AZDO_PROJECT"
|
|
59
|
-
AZDO_ORG=$(printf '%b' "${AZDO_ORG//%/\\x}")
|
|
60
|
-
AZDO_PROJECT=$(printf '%b' "${AZDO_PROJECT//%/\\x}")
|
|
61
|
-
|
|
62
|
-
if [ -z "$AZDO_ORG" ] || [ -z "$AZDO_PROJECT" ]; then
|
|
63
|
-
echo "❌ Could not extract organization/project from remote URL"
|
|
64
|
-
echo " Remote: $REMOTE_URL"
|
|
65
|
-
echo ""
|
|
66
|
-
echo "Ensure the remote URL follows one of these formats:"
|
|
67
|
-
echo " https://dev.azure.com/{ORG}/{PROJECT}/_git/{REPO}"
|
|
68
|
-
echo " https://{ORG}.visualstudio.com/{PROJECT}/_git/{REPO}"
|
|
69
|
-
echo " {ORG}@vs-ssh.visualstudio.com:v3/{ORG}/{PROJECT}/{REPO}"
|
|
70
|
-
# HALT — cannot proceed without org/project context
|
|
71
|
-
fi
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
When `AZDO_MODE == cli`, also configure the defaults so commands work correctly:
|
|
75
|
-
```bash
|
|
76
|
-
az devops configure --defaults organization="$AZDO_ORG_URL" project="$AZDO_PROJECT"
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
When `AZDO_MODE == rest`, store these for API calls:
|
|
80
|
-
- Base URL: `$AZDO_ORG_URL/$AZDO_PROJECT_URL_SAFE/_apis`
|
|
81
|
-
- Auth header: `Authorization: Basic $(echo -n ":$PAT" | base64)`
|
|
82
|
-
|
|
83
|
-
Report in pre-flight:
|
|
84
|
-
- ✅ `azdo context` — org: `{AZDO_ORG}`, project: `{AZDO_PROJECT}`
|
|
85
|
-
- ❌ `azdo context` — could not parse from remote URL → halt with instructions
|
|
86
|
-
|
|
87
|
-
Pass `{AZDO_MODE}`, `{AZDO_ORG}`, `{AZDO_PROJECT}`, `{AZDO_ORG_URL}` into
|
|
88
|
-
all phase prompts alongside `{PLATFORM}`.
|