@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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mad-skills",
3
3
  "description": "AI-assisted planning, development and governance tools",
4
- "version": "2.0.40",
4
+ "version": "2.0.42",
5
5
  "author": {
6
6
  "name": "slamb2k",
7
7
  "url": "https://github.com/slamb2k"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slamb2k/mad-skills",
3
- "version": "2.0.40",
3
+ "version": "2.0.42",
4
4
  "description": "Claude Code skills collection — full lifecycle development tools",
5
5
  "type": "module",
6
6
  "repository": {
@@ -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://dev.azure.com/$AZDO_ORG"
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=$(printf '%b' "${AZDO_ORG//%/\\x}")
335
- AZDO_PROJECT=$(printf '%b' "${AZDO_PROJECT//%/\\x}")
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 $(echo -n ":$PAT" | base64)"
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
@@ -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
@@ -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 `references/azdo-platform.md` for tooling
132
- detection (`AZDO_MODE`) and configuration validation (`AZDO_ORG`, `AZDO_PROJECT`).
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
  ---
@@ -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 `references/azdo-platform.md` for tooling
128
- detection (`AZDO_MODE`) and configuration validation (`AZDO_ORG`, `AZDO_PROJECT`).
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
  ---
@@ -1,12 +1,12 @@
1
1
  {
2
- "generated": "2026-03-18T00:48:31.500Z",
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": 445,
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": 378,
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": 431,
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": 419,
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": 343,
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": 418,
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": 253,
89
+ "lines": 272,
90
90
  "hasScripts": false,
91
91
  "hasReferences": true,
92
92
  "hasAssets": false,
@@ -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://dev.azure.com/$AZDO_ORG"
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=$(printf '%b' "${AZDO_ORG//%/\\x}")
175
- AZDO_PROJECT=$(printf '%b' "${AZDO_PROJECT//%/\\x}")
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 $(echo -n ":$PAT" | base64)`
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
@@ -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://dev.azure.com/$AZDO_ORG"
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=$(printf '%b' "${AZDO_ORG//%/\\x}")
192
- AZDO_PROJECT=$(printf '%b' "${AZDO_PROJECT//%/\\x}")
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 $(echo -n ":$PAT" | base64)`
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" or CHECKS.status == "no_checks":
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
- report failures to user, stop
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 $(echo -n ":{PAT}" | base64)"
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 $(echo -n ":{PAT}" | base64)"
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 $(echo -n ":$PAT" | base64)"
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
- # Wait for CI to start (max 2 min)
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 "$BRANCH" --top 5 \
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 "$BRANCH" --top 5 \
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 "$BRANCH" --top 5 \
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
- BUILDS_URL="$AZDO_ORG_URL/$AZDO_PROJECT_URL_SAFE/_apis/build/builds?branchName=refs/heads/$BRANCH&\$top=5&api-version=7.0"
176
+ BUILDS_BASE="$AZDO_ORG_URL/$AZDO_PROJECT_URL_SAFE/_apis/build/builds"
149
177
 
150
- # Wait for CI to start (max 2 min)
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
- RUN_COUNT=$(curl -s -H "$AUTH" "$BUILDS_URL" | jq '.value | length')
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
- EVALS=$(curl -s -H "$AUTH" \
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
- EVAL_COUNT=$(echo "$EVALS" | jq '.value | length')
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
- BUILDS_JSON=$(curl -s -H "$AUTH" "$BUILDS_URL")
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
- BUILDS_JSON=$(curl -s -H "$AUTH" "$BUILDS_URL")
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
- EVALS=$(curl -s -H "$AUTH" \
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
- REJECTED=$(echo "$EVALS" | jq '[.value[] | select(.status=="rejected")] | length')
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
- # Check for rejected policies
69
- REJECTED=$(az repos pr policy list --id "$PR_NUMBER" \
70
- --org "$AZDO_ORG_URL" --project "$AZDO_PROJECT" \
71
- --query "[?status=='rejected'] | length(@)" -o tsv 2>/dev/null)
72
- if [ -n "$REJECTED" ] && [ "$REJECTED" != "0" ]; then
73
- STATUS="failed"
74
- ERRORS="$REJECTED PR policies are rejected"
75
- MERGE_COMMIT=""; BRANCH_DELETED=false
76
- emit_report; exit 1
77
- fi
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" --project "$AZDO_PROJECT" \
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" --project "$AZDO_PROJECT" \
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 $(echo -n ":$PAT" | base64)"
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
- # Check for rejected policies
115
- EVALS=$(curl -s -H "$AUTH" \
116
- "$AZDO_ORG_URL/$AZDO_PROJECT_URL_SAFE/_apis/policy/evaluations?artifactId=vstfs:///CodeReview/CodeReviewId/$AZDO_PROJECT_URL_SAFE/$PR_NUMBER&api-version=7.0")
117
- REJECTED=$(echo "$EVALS" | jq '[.value[] | select(.status=="rejected")] | length')
118
- if [ "$REJECTED" != "0" ]; then
119
- STATUS="failed"
120
- ERRORS="PR has rejected policy evaluations"
121
- MERGE_COMMIT=""; BRANCH_DELETED=false
122
- emit_report; exit 1
123
- fi
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
@@ -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}`.