@sassywares/skills 0.5.2 → 0.5.4

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/README.md CHANGED
@@ -133,8 +133,11 @@ knowledge:
133
133
  write: true # false = this project takes no learnings
134
134
  git:
135
135
  repo: origin # a remote name | workspace (repos below, none here) | none (no git)
136
- # remote: org/backup # workspace only: the snapshot mirror; required when repo: workspace
137
- default_branch: main # what branches are cut from and PRs target
136
+ base_map: # head branch -> the base it targets
137
+ "*": staging # ordinary work branches from and targets staging
138
+ staging: main # staging releases to main
139
+ # remote: owner/backup # workspace only: the snapshot mirror
140
+ # mirror_branch: main # workspace only: the mirror's branch
138
141
  repos: # workspace only: each nested repo -> the base its PRs target
139
142
  fe: dev
140
143
  commit:
@@ -143,17 +146,20 @@ pr:
143
146
  babysit: true
144
147
  merge: false # true authorizes merging every babysat PR here
145
148
  delete_branch: false
146
- hooks: # project steps injected into the PR pipeline
149
+ hooks:
147
150
  pr:
148
- before_create: [] # after the commit, before the PR opens
149
- after_create: [] # the moment the PR exists
150
- before_merge: [] # this run is about to merge
151
- after_merge: [] # this run observed the merge
151
+ "*": # keyed by the PR's HEAD branch
152
+ before_create: [tap2-version mode:auto] # after the commit, before the PR opens
153
+ after_create: [] # the moment the PR exists
154
+ before_merge: [] # this run is about to merge
155
+ after_merge: [] # this run observed the merge
156
+ staging: # a per-point override for one head
157
+ before_create: []
152
158
  ```
153
159
 
154
160
  ### Extending the pipeline
155
161
 
156
- A project adds its own steps by naming a skill that lives in its own `.agents/skills/`. Nothing declared means nothing runs.
162
+ A project adds its own steps by naming a skill that lives in its own `.agents/skills/`. Nothing declared means nothing runs. Keys are the **head** branch — the one a PR comes from — and every point can be overridden per head; entries may carry args (`<skill> <args…>`).
157
163
 
158
164
  | Hook | Runs | Typical use |
159
165
  |---|---|---|
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sassywares/skills",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "Lean, explicit-invocation Agent Skills for shipping fast: plan, work, review, validate, ship.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -21,7 +21,9 @@ Keep an open PR moving toward merge by reacting to three streams as each arrives
21
21
 
22
22
  An explicit token always beats the config.
23
23
 
24
- **Config.** Resolve `.sassywares/config.yaml` (nearest, walking up) per the `sassy-setup` skill's `references/config.md`. Precedence: explicit token > session instruction > config > skill default. `pr.*` seeds `merge` and `delete_branch`; `hooks.pr.before_merge` and `hooks.pr.after_merge` inject project steps around a merge.
24
+ **Config.** Resolve `.sassywares/config.yaml` (nearest, walking up) per the `sassy-setup` skill's `references/config.md`. Precedence: explicit token > session instruction > config > skill default. `pr.*` seeds `merge` and `delete_branch`; merge hooks come from `hooks.pr` for this run's head.
25
+
26
+ **Keys are the head branch.** Resolve per head: the exact head, then the longest matching family (`release/*`), then `"*"`. Each point overrides independently. An entry is `<skill> <args…>` — split on the first whitespace and pass the args verbatim.
25
27
 
26
28
  **One PR per run.** This skill watches the PR resolved from the argument or the current branch. A workspace fan-out is N runs, one per PR, and `pr.merge` never merges a fan-out — that needs an explicit say-so per PR.
27
29
 
@@ -35,7 +37,7 @@ An explicit token always beats the config.
35
37
 
36
38
  Snapshot the PR state first, then act in this order:
37
39
 
38
- 1. **Terminal** — `MERGED`/`CLOSED` → if it is `MERGED` and `hooks.pr.after_merge` is declared, run those hooks once (see Step 3) before stopping.
40
+ 1. **Terminal** — `MERGED`/`CLOSED` → if it is `MERGED` and `after_merge` resolves for this PR's head, run those hooks once (see Step 3) before stopping.
39
41
  2. **Capture the head SHA** — a later push invalidates this snapshot's CI.
40
42
  3. **Feedback before CI** — fetch open review threads (`gh pr view --json reviewThreads,comments`). Address each actionable thread: reply, then fix. Use `sassy-debug` for a real defect. Never wait for a reviewer who is "still reviewing" before acting on feedback already posted.
41
43
  4. **Stale-SHA cancellation** — if the head moved since step 2, skip the CI work for this snapshot.
@@ -50,7 +52,7 @@ Stop when any holds:
50
52
  - **Terminal** — merged or closed.
51
53
  - **Looks ready** — `MERGEABLE`, `CLEAN`, all checks terminal, no open review threads, no currency blocker, zero `needs-human` items. With `merge:on`, run the merge (and `delete-branch:on` branch deletion if set) before treating this as a stop; the merge turns it into the **Terminal** stop.
52
54
 
53
- **Merge hooks.** Immediately before merging, run `hooks.pr.before_merge` in declared order — one `worker` per hook, with the same brief every hook gets: plan path, changed files, artifact paths, base, branch, commit hash, and the PR URL plus head SHA. These run before the merge, so they must leave the tree clean. A missing skill or a `blocked` result means **do not merge**: report it and stop at Blocked. After a successful merge, run `hooks.pr.after_merge` the same way; these follow the merge, so they must leave the tree clean. In a workspace fan-out, everything is per PR.
55
+ **Merge hooks.** Immediately before merging, resolve `before_merge` for this PR's head and run each entry in declared order — one `worker` per hook, with the same brief every hook gets: plan path, changed files, artifact paths, base, branch, commit hash, and the PR URL plus head SHA. These run before the merge, so they must leave the tree clean. A missing skill or a `blocked` result means **do not merge**: report it and stop at Blocked. After a successful merge, resolve `after_merge` for this PR's head and run each entry the same way; these follow the merge, so they must leave the tree clean. In a workspace fan-out, everything is per PR.
54
56
 
55
57
  **Which merge hook fires.** `before_merge` needs a merge this run will perform. `after_merge` fires whenever this run observes the PR merged, including one that was already merged when the run started — so it must be idempotent.
56
58
 
@@ -30,7 +30,7 @@ Treat the reads as a snapshot; re-read branch and staged set right before commit
30
30
 
31
31
  **Config.** Resolve `.sassywares/config.yaml` (nearest, walking up) per the `sassy-setup` skill's `references/config.md`. Precedence: explicit token > session instruction > config > skill default. `commit.branch` supplies the default.
32
32
 
33
- **Branch option.** `branch:current|new [<name>]`. `current` commits on the checked-out branch. `new` cuts a branch from `origin/<base>` — `<base>` is `repos[<dir>]` in a workspace config, else `base_map[<current head>]` (falling back to `base_map["*"]`), else the probed base **confirmed with the user** — named `<name>` when given, else from the change. `sassy-commit-push-pr` forwards its own `branch` choice here.
33
+ **Branch option.** `branch:current|new [<name>]`. `current` commits on the checked-out branch. `new` cuts a branch from `origin/<base>` — `<base>` is `base_map` from the repo's own config when it has one, else `repos[<dir>]` from the workspace config, else the probed base **confirmed with the user** — named `<name>` when given, else from the change. `sassy-commit-push-pr` forwards its own `branch` choice here.
34
34
 
35
35
  **Not in a workspace root, not outside git.** With `git.repo: workspace` there is no local commit to make: refuse in one line and name the snapshot instead. With `git.repo: none`, refuse and say the project is deliberately outside git. Inside a nested repo, a normal commit is correct — the nearest work tree is the repo.
36
36
 
@@ -16,7 +16,9 @@ disable-model-invocation: true
16
16
 
17
17
  **Options.** `branch:new|current` selects the branch policy below. `babysit:on|off` (default `on`) controls the closing babysit. `checkpoint` bounds that babysit to a single tick. `merge:on|off` and `delete-branch:on|off` (both default `off`) are forwarded unchanged to `sassy-babysit`.
18
18
 
19
- **Config.** Resolve `.sassywares/config.yaml` (nearest, walking up) per the `sassy-setup` skill's `references/config.md`. Precedence: explicit token > session instruction > config > skill default. `commit.branch` seeds the branch policy, `pr.*` seeds the babysit options, and `hooks.pr.before_create` / `hooks.pr.after_create` inject project steps around the PR.
19
+ **Config.** Resolve `.sassywares/config.yaml` (nearest, walking up) per the `sassy-setup` skill's `references/config.md`. Precedence: explicit token > session instruction > config > skill default. `commit.branch` seeds the branch policy, `pr.*` seeds the babysit options, and `hooks.pr` supplies the `before_create` / `after_create` steps for this run's head.
20
+
21
+ **Keys are the head branch.** Resolve per head: the exact head, then the longest matching family (`release/*`), then `"*"`. Each point overrides independently. An entry is `<skill> <args…>` — split on the first whitespace and pass the args verbatim.
20
22
 
21
23
  ## Context
22
24
 
@@ -37,14 +39,14 @@ Only an exit-0 empty list means "no open PR". A non-zero exit means **unknown**
37
39
 
38
40
  **Branch policy.** Default: if on the base branch or detached HEAD, create a feature branch named from the work and say so. `branch:current` or a plain request keeps the checked-out branch (that run pushes and skips the PR). Never ask whether to branch. Forward the resolved choice to `sassy-commit`: `branch:new <name>` when this run creates a branch, else `branch:current`.
39
41
 
40
- **Base branch.** `<base>` is `repos[<repo dir>]` from a workspace config, else `base_map[<current head>]` falling back to `base_map["*"]`, else the probed remote HEAD **confirmed with the user before use**. The map is the reason a release PR targets `main` while the work sits on `staging`, with no token passed. An explicit `base:<branch>` token wins over all of them — use it when this PR targets something other than the day-to-day base, such as a release PR aimed at `main` while work lands on `staging`. **Never use `defaultBranchRef` as the PR base when the config names one.** Pass it explicitly: `gh pr create --base <base>`.
42
+ **Base branch.** `<base>` is `base_map[<current head>]` from the nearest config — a nested repo's own config wins over the workspace's `repos[<dir>]` — falling back to `base_map["*"]`, else the probed remote HEAD **confirmed with the user before use**. The map is the reason a release PR targets `main` while the work sits on `staging`, with no token passed. An explicit `base:<branch>` token wins over all of them — use it when this PR targets something other than the day-to-day base, such as a release PR aimed at `main` while work lands on `staging`. **Never use `defaultBranchRef` as the PR base when the config names one.** Pass it explicitly: `gh pr create --base <base>`.
41
43
 
42
44
  **Refused topologies.** From a `workspace` root there is no single PR to open — refuse in one line and point at `sassy-ship`, which owns the per-repo fan-out. With `git.repo: none`, refuse and say why. Neither ever pushes directly to a base branch.
43
45
 
44
46
  ## Steps
45
47
 
46
48
  1. **Commit** — follow `sassy-commit` with the branch choice from the policy above (named files only, outcome-stating message). Honor an `exclude:<paths>` token if present.
47
- 2. **`before_create` hooks.** Read `hooks.pr.before_create` from the config; empty means skip. Run each name in declared order, one `worker` per hook, whose task is *read `<repo>/.agents/skills/<name>/SKILL.md` and follow it* plus a brief: plan path, changed files, artifact paths, base, branch, and the commit hash. This point runs **after** the commit, so a hook may write and commit its own artifacts — push whatever ends up on the branch. A missing skill or a `blocked` result stops the run and names the hook; a `needs-decision` result comes back to you to present.
49
+ 2. **`before_create` hooks.** Resolve `before_create` for the head branch (`base_map` decides the base, the head is the branch you are on); empty means skip. Split each entry on the first whitespace — the first token is the skill, the rest are its args. Run each name in declared order, one `worker` per hook, whose task is *read `<repo>/.agents/skills/<name>/SKILL.md` and follow it* plus a brief: plan path, changed files, artifact paths, base, branch, and the commit hash. This point runs **after** the commit, so a hook may write and commit its own artifacts — push whatever ends up on the branch. A missing skill or a `blocked` result stops the run and names the hook; a `needs-decision` result comes back to you to present.
48
50
  3. **Pre-push gate** — resolve any pre-push or review-ready requirement from the project's active instructions. Only evidence valid for the exact commit being sent satisfies it; otherwise stop and report what is missing. If none, proceed.
49
51
  4. **Push** — `git push -u origin <branch>` for a new branch, else `git push`. Re-verify branch and remote first, and re-check that the branch is ahead of origin after any hook commits.
50
52
  5. **Compose the PR title and body**
@@ -31,4 +31,4 @@ If a skill tells you another skill is missing, run `skills:sync` and restart. Su
31
31
 
32
32
  ## Hooks
33
33
 
34
- `hooks.pr.*` names project skills that run inside the PR pipeline: `before_create`, `after_create`, `before_merge`, `after_merge`. They are ordinary skills in `.agents/skills/`, which means they are **code** — review them like code, and never point a hook at a copy of a suite skill.
34
+ `hooks.pr` names project skills that run inside the PR pipeline, keyed by the PR's **head** branch: `"*"` for every PR, an exact branch, or a family such as `release/*`, overriding per point (`before_create`, `after_create`, `before_merge`, `after_merge`). An entry is `<skill> <args…>`; the args mean whatever that skill says they mean — `mode:auto` pre-authorizes the step. Hooks are ordinary skills in `.agents/skills/`, which makes them **code**: review them like code, and never point a hook at a copy of a suite skill.
@@ -157,8 +157,9 @@ feature/x --base_map["*"]--> staging --base_map[staging]--> main
157
157
  ```
158
158
 
159
159
  1. `base_map[<head>]` — the exact rung
160
- 2. `base_map["*"]` — ordinary work
161
- 3. stop and ask; never guess
160
+ 2. the longest matching family key (`release/*`) — a nested rung
161
+ 3. `base_map["*"]` — ordinary work
162
+ 4. stop and ask; never guess
162
163
 
163
164
  A rung is a release edge: standing on `staging`, a PR targets `main` without
164
165
  anyone passing `base:main`. Add a rung for any branch that releases elsewhere; a
@@ -169,10 +170,21 @@ The probed remote default is only useful as a hint for `"*"`: it tells you where
169
170
  the repository considers work to land, not what every branch targets. Confirm it
170
171
  before writing.
171
172
 
173
+ `--head <branch>` prints the resolution for one head: the base (repo topology)
174
+ and all four hook points. Use it instead of merging maps by hand.
175
+
172
176
  In a `workspace`, `base_map` does not apply — there are no branches at the
173
- workspace level. Nested repos carry their own bases in `repos`, and a nested repo
174
- that needs a ladder gets its own `.sassywares/config.yaml` rather than a nested
175
- map here.
177
+ workspace level. A nested repo's base resolves in this order:
178
+
179
+ 1. that repo's own `.sassywares/config.yaml` → `base_map` — **authoritative**
180
+ 2. `repos[<dir>]` in the workspace config — the inventory, and the fallback for a
181
+ repo that carries no config
182
+ 3. stop and ask
183
+
184
+ `repos` exists because a workspace can hold repos you do not own and cannot add a
185
+ config to. When a repo has both, the nested config wins and `--check` reports
186
+ the disagreement rather than picking silently. A nested config needs no ladder
187
+ of its own unless its PRs target more than one branch.
176
188
 
177
189
  **Branch name**, one per run. Derived once from the plan slug or topic before the
178
190
  first commit, then passed to every repo. Multi-repo runs pass
@@ -230,9 +242,9 @@ Per point, independently:
230
242
  3. `"*"`
231
243
 
232
244
  The first rule that declares a value for that point wins and the rest are not
233
- merged. So a `staging` entry that sets only `before_create` still inherits
234
- `after_merge` from `"*"`. Declaring the same point in two families that both
235
- match one head is a configuration error, not a precedence puzzle.
245
+ merged, so a `staging` entry that sets only `before_create` still inherits
246
+ `after_merge` from `"*"`. When two families match one head, the longer prefix
247
+ wins — `release/v2/*` beats `release/*`.
236
248
 
237
249
  ### Entries carry arguments
238
250
 
@@ -6,6 +6,7 @@
6
6
  #
7
7
  # Usage:
8
8
  # detect.sh [--root DIR] print a proposed config to stdout
9
+ # detect.sh [--head BRANCH] resolve the base and hooks for one head
9
10
  # detect.sh [--root DIR] --check compare the existing config to reality
10
11
  set -euo pipefail
11
12
 
@@ -130,10 +131,13 @@ yaml_flat() {
130
131
  path = ""
131
132
  for (i = 1; i <= sp; i++) if (sd[i] < d) path = path (path == "" ? "" : us) sk[i]
132
133
  print path (path == "" ? "" : us) key "\t" val
133
- }' "$CONFIG"
134
+ }' "${1:-$CONFIG}"
134
135
  }
135
136
 
136
- yflat() { yaml_flat | awk -F'\t' -v p="$1" '$1 == p { print $2; exit }'; }
137
+ yflat() { yaml_flat "$CONFIG" | awk -F'\t' -v p="$1" '$1 == p { print $2; exit }'; }
138
+
139
+ # yflat_in FILE PATH — the same reader against another config file.
140
+ yflat_in() { yaml_flat "$1" | awk -F'\t' -v p="$2" '$1 == p { print $2; exit }'; }
137
141
 
138
142
  list_items() {
139
143
  printf '%s' "$1" | sed -e 's/^\[//' -e 's/\]$//' | tr ',' '\n' \
@@ -154,7 +158,7 @@ map_match() {
154
158
  if [[ "$head" == "$stem"* ]] && (( ${#stem} > bestlen )); then
155
159
  best="$pat"; bestlen=${#stem}
156
160
  fi
157
- done < <(yaml_flat | awk -F'\t' -v us="$US" -v sec="$section" \
161
+ done < <(yaml_flat "$CONFIG" | awk -F'\t' -v us="$US" -v sec="$section" \
158
162
  '{n=split($1,a,us); if (n==3 && a[1]=="git" && a[2]==sec) print a[3]}' | sort -u)
159
163
  if [[ -n "$best" ]]; then
160
164
  v="$(yflat "git${US}${section}${US}${best}")"
@@ -290,6 +294,9 @@ if (( CHECK )); then
290
294
  fi
291
295
 
292
296
  if [[ "$TOPOLOGY" == "repo" ]]; then
297
+ if [[ -n "$(yflat "git${US}remote")" ]]; then
298
+ bad "git.remote is workspace-only — a repo config names its remote in git.repo"
299
+ fi
293
300
  if [[ -z "$(yflat "git${US}base_map${US}*")" ]]; then
294
301
  bad "git.base_map has no \"*\" entry — it is the base for ordinary work"
295
302
  else
@@ -299,7 +306,7 @@ if (( CHECK )); then
299
306
  "*") ;;
300
307
  *"*") [[ "${pat%\*}" != *"*" ]] || bad "base_map key '$pat': only one trailing * is allowed" ;;
301
308
  esac
302
- done < <(yaml_flat | awk -F'\t' -v us="$US" '{n=split($1,a,us); if (n==3 && a[1]=="git" && a[2]=="base_map") print a[3]}' | sort -u)
309
+ done < <(yaml_flat "$CONFIG" | awk -F'\t' -v us="$US" '{n=split($1,a,us); if (n==3 && a[1]=="git" && a[2]=="base_map") print a[3]}' | sort -u)
303
310
  head_ref="${HEAD_ARG:-$(git -C "$ROOT" branch --show-current 2>/dev/null || true)}"
304
311
  if [[ -n "$head_ref" ]]; then
305
312
  # the --head readout below prints this line; avoid printing it twice
@@ -339,8 +346,22 @@ if (( CHECK )); then
339
346
  [[ -n "$name" ]] || continue
340
347
  base="$(detect_base "$ROOT/$name")"
341
348
  listed="$(block_value repos "$name" || true)"
342
- if [[ -z "$listed" ]]; then
343
- bad "repos has no entry for '$name' (remote default: ${base:-unknown})"
349
+ # A nested repo's own config is authoritative; `repos` is the inventory and
350
+ # the fallback for repos that have none.
351
+ nested="$ROOT/$name/.sassywares/config.yaml"
352
+ nested_base=""
353
+ [[ -f "$nested" ]] && nested_base="$(yflat_in "$nested" "git${US}base_map${US}*")"
354
+
355
+ if [[ -n "$nested_base" ]]; then
356
+ if [[ -n "$listed" && "$listed" != "$nested_base" ]]; then
357
+ bad "repos.$name is '$listed' but $name/.sassywares/config.yaml maps \"*\" to '$nested_base' — the nested config wins; fix or drop the repos entry"
358
+ elif [[ -z "$listed" ]]; then
359
+ note " $name: base '$nested_base' from its own config (no repos entry needed)"
360
+ else
361
+ note " $name: base '$nested_base' (agrees with its own config)"
362
+ fi
363
+ elif [[ -z "$listed" ]]; then
364
+ bad "repos has no entry for '$name' and $name carries no config (remote default: ${base:-unknown})"
344
365
  elif [[ -n "$base" && "$listed" != "$base" ]]; then
345
366
  note " repos.$name '$listed' overrides the remote default '$base' — expected for an integration branch"
346
367
  fi
@@ -454,7 +475,8 @@ EOF
454
475
  else
455
476
  printf ' "*": %s # base for ordinary work; never guessed\n' "<confirm by hand>"
456
477
  fi
457
- printf ' # staging: main # uncomment when a branch releases somewhere else\n'
478
+ printf ' # staging: main # uncomment when a branch releases somewhere else\n'
479
+ printf ' # release/*: main # a family of heads, longest prefix wins\n'
458
480
  cat <<'EOF'
459
481
 
460
482
  # workspace only: each nested repo folder -> the base its PRs target
@@ -22,14 +22,14 @@ disable-model-invocation: true
22
22
 
23
23
  ## Steps (in order)
24
24
 
25
- 1. **Review gate.** In `mode:manual` only: present a compact shipping summary — the plan or topic, the changes from the cycle results, the docs the close-out expects to write, the branch/PR target, the babysit options, and **the hooks this run will trigger, by point and name, or that none are declared** — and wait. In `mode:auto`, skip this gate.
25
+ 1. **Review gate.** In `mode:manual` only: present a compact shipping summary — the plan or topic, the changes from the cycle results, the docs the close-out expects to write, the branch/PR target, the babysit options, and **the hooks resolved for this run's head — point, skill, and args — or that none are declared** — and wait. In `mode:auto`, skip this gate.
26
26
  2. **Compound (conditional).** If the work produced a qualifying learning, dispatch a `worker` whose task is: *load the `sassy-compound` skill and follow it with `mode:auto`* — with a compact learnings brief (the solved problems, root causes, and solutions taken from the cycle results, not from re-reading the tree). It always gets `mode:auto` because ship's own gate already covered the approval. Skip cleanly when nothing qualifies, and skip entirely when `knowledge.write` is `false`.
27
27
  3. **Handbook (conditional).** If the feature is self-contained and independent enough that a teammate would benefit from a handover, dispatch a `worker` whose task is: *load the `sassy-handbook` skill and follow it* — with the plan path and topic. The worker reads the tree itself. Skip for small or tightly-coupled changes.
28
28
  4. **Commit or snapshot.** After the review gate — never before it — the docs and any remaining code become durable.
29
29
  - **`git.repo` is a remote.** Dispatch a `worker`: *load the `sassy-commit` skill and follow it*, naming that the commit must include the plan, any new solutions docs, the concepts file, `AGENTS.md`, and any handbook just written — resolved from `artifacts.*`, never hardcoded. Use `branch:current` when already on a feature branch, so the docs land with the phase commits. When the checked-out branch is the base branch, use `branch:new <name>` so the branch is created from `origin/<base>` and carries every phase commit; a workspace run passes the run's shared branch name. Never push directly to the base branch. It returns the hash and subject.
30
30
  - **`git.repo: workspace`.** Commit code inside each affected repo on the run's shared branch (cut once on the run's first commit, `branch:current` after), then run `.sassywares/workspace/snapshot`. The snapshot is the workspace's commit and it runs every time, right after approval. A missing or outdated `.sassywares/workspace/snapshot` is a blocker: report it and say to run `sassy-setup`.
31
31
  - **`git.repo: none`.** No commit exists here. Report that the work is not persisted and stop.
32
- 5. **PR.** In a `remote` repo, dispatch a `worker`: *load the `sassy-commit-push-pr` skill and follow it with `branch:current` and `babysit:off`, forwarding `base:<branch>` when the caller supplied one* (step 6 owns the babysit). It returns the PR URL. In a `workspace`, open one PR per affected repo — the same branch name in each, `--base` from that repo's `repos` entry — and return a table. A touched repo missing from `repos`, or a base that cannot be resolved, is a stop-and-ask, never a guess. `none` opens nothing.
32
+ 5. **PR.** In a `remote` repo, dispatch a `worker`: *load the `sassy-commit-push-pr` skill and follow it with `branch:current` and `babysit:off`, forwarding `base:<branch>` when the caller supplied one* (step 6 owns the babysit). It returns the PR URL. In a `workspace`, open one PR per affected repo — the same branch name in each, `--base` from that repo's own `base_map` when it carries a `.sassywares/config.yaml`, else its `repos` entry — and return a table. A touched repo missing from `repos`, or a base that cannot be resolved, is a stop-and-ask, never a guess. `none` opens nothing.
33
33
 
34
34
  **The PR step owns the PR hooks.** `before_create` and `after_create` run inside `sassy-commit-push-pr`, once per PR; ship neither runs nor repeats them.
35
35
  6. **Babysit.** With `pr.babysit` (default `true`), dispatch a `worker`: *load the `sassy-babysit` skill and follow it for the PR just created, one checkpoint*, forwarding any `merge:on|off` and `delete-branch:on|off` the caller supplied, else `pr.merge` and `pr.delete_branch`. It reports looks-ready or blocked. For a continuous watch, tell the user to run `sassy-babysit` in-session.