@sassywares/skills 0.5.3 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sassywares/skills",
3
- "version": "0.5.3",
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",
@@ -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
 
@@ -39,7 +39,7 @@ Only an exit-0 empty list means "no open PR". A non-zero exit means **unknown**
39
39
 
40
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`.
41
41
 
42
- **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>`.
43
43
 
44
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.
45
45
 
@@ -174,9 +174,17 @@ before writing.
174
174
  and all four hook points. Use it instead of merging maps by hand.
175
175
 
176
176
  In a `workspace`, `base_map` does not apply — there are no branches at the
177
- workspace level. Nested repos carry their own bases in `repos`, and a nested repo
178
- that needs a ladder gets its own `.sassywares/config.yaml` rather than a nested
179
- 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.
180
188
 
181
189
  **Branch name**, one per run. Derived once from the plan slug or topic before the
182
190
  first commit, then passed to every repo. Multi-repo runs pass
@@ -131,10 +131,13 @@ yaml_flat() {
131
131
  path = ""
132
132
  for (i = 1; i <= sp; i++) if (sd[i] < d) path = path (path == "" ? "" : us) sk[i]
133
133
  print path (path == "" ? "" : us) key "\t" val
134
- }' "$CONFIG"
134
+ }' "${1:-$CONFIG}"
135
135
  }
136
136
 
137
- 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 }'; }
138
141
 
139
142
  list_items() {
140
143
  printf '%s' "$1" | sed -e 's/^\[//' -e 's/\]$//' | tr ',' '\n' \
@@ -155,7 +158,7 @@ map_match() {
155
158
  if [[ "$head" == "$stem"* ]] && (( ${#stem} > bestlen )); then
156
159
  best="$pat"; bestlen=${#stem}
157
160
  fi
158
- 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" \
159
162
  '{n=split($1,a,us); if (n==3 && a[1]=="git" && a[2]==sec) print a[3]}' | sort -u)
160
163
  if [[ -n "$best" ]]; then
161
164
  v="$(yflat "git${US}${section}${US}${best}")"
@@ -303,7 +306,7 @@ if (( CHECK )); then
303
306
  "*") ;;
304
307
  *"*") [[ "${pat%\*}" != *"*" ]] || bad "base_map key '$pat': only one trailing * is allowed" ;;
305
308
  esac
306
- 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)
307
310
  head_ref="${HEAD_ARG:-$(git -C "$ROOT" branch --show-current 2>/dev/null || true)}"
308
311
  if [[ -n "$head_ref" ]]; then
309
312
  # the --head readout below prints this line; avoid printing it twice
@@ -343,8 +346,22 @@ if (( CHECK )); then
343
346
  [[ -n "$name" ]] || continue
344
347
  base="$(detect_base "$ROOT/$name")"
345
348
  listed="$(block_value repos "$name" || true)"
346
- if [[ -z "$listed" ]]; then
347
- 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})"
348
365
  elif [[ -n "$base" && "$listed" != "$base" ]]; then
349
366
  note " repos.$name '$listed' overrides the remote default '$base' — expected for an integration branch"
350
367
  fi
@@ -29,7 +29,7 @@ disable-model-invocation: true
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.