@sassywares/skills 0.4.3 → 0.5.1
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 +1 -1
- package/skills/sassy-commit/SKILL.md +1 -1
- package/skills/sassy-commit-push-pr/SKILL.md +1 -1
- package/skills/sassy-setup/SKILL.md +2 -2
- package/skills/sassy-setup/assets/snapshot +1 -1
- package/skills/sassy-setup/references/config.md +78 -41
- package/skills/sassy-setup/scripts/detect.sh +181 -55
- package/skills/sassy-skill/SKILL.md +7 -3
package/package.json
CHANGED
|
@@ -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 `
|
|
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.
|
|
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
|
|
|
@@ -37,7 +37,7 @@ Only an exit-0 empty list means "no open PR". A non-zero exit means **unknown**
|
|
|
37
37
|
|
|
38
38
|
**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
39
|
|
|
40
|
-
**Base branch.** `<base>` is `repos[<repo dir>]` from a workspace config, else `
|
|
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>`.
|
|
41
41
|
|
|
42
42
|
**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
43
|
|
|
@@ -26,9 +26,9 @@ disable-model-invocation: true
|
|
|
26
26
|
- **`none`** — confirm the intent: this folder is deliberately outside git, so commit, PR, and babysit refuse here.
|
|
27
27
|
- **Artifacts** — offer the detected paths as the default; take overrides.
|
|
28
28
|
- **Knowledge** — `knowledge.read` defaults to the solutions dir plus the concepts file. Ask whether there are extra folders to read (a hand-written `notes/learnings/`, a `docs/adr/`).
|
|
29
|
-
- **Base
|
|
29
|
+
- **Base map** — detection proposes `"*"` from the probed remote default. Ask where ordinary work branches from, and whether the default branch itself releases to another branch (a `staging: main` rung). Never write `main` as a guess. In `workspace` mode, show the `repos` map and get a yes on every row — a probed base is only the remote's default.
|
|
30
30
|
- **`pr.*`** — default `babysit: true`, `merge: false`, `delete_branch: false`. Say plainly that `merge: true` authorizes merging every babysat PR in this project without asking.
|
|
31
|
-
- **Hooks** — offer, do not impose. Ask whether the project wants steps injected at `before_create`, `after_create`, `before_merge`, or `after_merge`, and
|
|
31
|
+
- **Hooks** — offer, do not impose. Ask whether the project wants steps injected at `before_create`, `after_create`, `before_merge`, or `after_merge`, and under which head branch (`"*"` for all). Entries may carry args (`<skill> <args…>`); a hook skill's args are its own vocabulary. Leave them `[]` when there is nothing to add. **Never write a hook skill yourself** — hooks are project skills, and copying one of ours into that role is the fork this mechanism exists to prevent.
|
|
32
32
|
4. **Write the config.** Build the file from the proposal plus the answers. It is written **fully commented**: every key keeps a description and its allowed values, exactly as the detector emits them. Never strip those comments to save lines — for a teammate opening the file, the comments are the documentation. If a config already exists, show what changes and ask before writing — never silently overwrite. Keep every key; an unset key falls back to its documented default, so a short file is legal when nothing deviates.
|
|
33
33
|
5. **Wire the skills.** The suite is installed from upstream with the open `skills` CLI, which vendors the skill files into the project and pins them. Never copy skill files in by hand, never add the suite as a dependency.
|
|
34
34
|
|
|
@@ -42,11 +42,13 @@ knowledge:
|
|
|
42
42
|
write: true
|
|
43
43
|
git:
|
|
44
44
|
repo: origin # <remote name> | workspace | none
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
base_map: # head branch -> the base it targets
|
|
46
|
+
"*": staging # ordinary work branches from and targets staging
|
|
47
|
+
staging: main # staging releases to main
|
|
48
|
+
remote: owner/backup # workspace only: the snapshot mirror
|
|
49
|
+
mirror_branch: main # workspace only: the mirror's branch
|
|
47
50
|
repos: # workspace only: sibling dir -> PR base
|
|
48
51
|
fe: dev
|
|
49
|
-
knit-ai-pipeline: master
|
|
50
52
|
commit:
|
|
51
53
|
branch: current # current | new
|
|
52
54
|
pr:
|
|
@@ -55,10 +57,10 @@ pr:
|
|
|
55
57
|
delete_branch: false
|
|
56
58
|
hooks:
|
|
57
59
|
pr:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
"*":
|
|
61
|
+
before_create: [tap2-version mode:auto]
|
|
62
|
+
staging:
|
|
63
|
+
before_create: []
|
|
62
64
|
```
|
|
63
65
|
|
|
64
66
|
| Key | Default | Meaning |
|
|
@@ -70,14 +72,15 @@ hooks:
|
|
|
70
72
|
| `knowledge.read` | `[docs/solutions, CONCEPTS.md]` | Paths scouted for prior learnings |
|
|
71
73
|
| `knowledge.write` | `true` | `false` = this project takes no learnings |
|
|
72
74
|
| `git.repo` | `origin` | Remote name, or a reserved value (below) |
|
|
75
|
+
| `git.base_map` | probed `"*"` | Head branch → the base it targets. `"*"` is required |
|
|
73
76
|
| `git.remote` | mirror of `git.repo` | Workspace only: mirror remote (URL or `owner/name`) |
|
|
74
|
-
| `git.
|
|
77
|
+
| `git.mirror_branch` | `main` | Workspace only: the branch the mirror pushes to |
|
|
75
78
|
| `repos` | `{}` | Workspace only: repo dir name → the base its PRs target |
|
|
76
79
|
| `commit.branch` | `current` | `current` commits where you are; `new` cuts a branch |
|
|
77
80
|
| `pr.babysit` | `true` | Whether close-out babysits the PR it opened |
|
|
78
81
|
| `pr.merge` | `false` | `true` authorizes merging a single babysat PR |
|
|
79
82
|
| `pr.delete_branch` | `false` | Delete the head branch once merged |
|
|
80
|
-
| `hooks.pr
|
|
83
|
+
| `hooks.pr.<pattern>.<point>` | `[]` | Project steps, keyed by the PR's head branch |
|
|
81
84
|
|
|
82
85
|
Paths are relative to the config's own directory, never to the cwd.
|
|
83
86
|
|
|
@@ -91,8 +94,9 @@ Paths are relative to the config's own directory, never to the cwd.
|
|
|
91
94
|
|
|
92
95
|
`workspace` and `none` are reserved words; a remote cannot be named either.
|
|
93
96
|
|
|
94
|
-
`remote` and `
|
|
95
|
-
|
|
97
|
+
`base_map` is repo-only. `remote` and `mirror_branch` are workspace-only and
|
|
98
|
+
describe the snapshot mirror — in a repo config the remote is simply
|
|
99
|
+
the value of `git.repo`.
|
|
96
100
|
In a workspace config, both describe **the snapshot mirror**: where it pushes,
|
|
97
101
|
and its branch.
|
|
98
102
|
|
|
@@ -145,21 +149,30 @@ dropped; the refusal names why and what to run instead.
|
|
|
145
149
|
even when the resolved config says `workspace`. Ran from `fe/`, work commits in
|
|
146
150
|
`fe/`. Ran from the workspace root (no repo), the workspace rules above apply.
|
|
147
151
|
|
|
148
|
-
**Base branch
|
|
152
|
+
**Base branch of a head.** `base_map` answers one question — *given this head,
|
|
153
|
+
what does a PR target?* — so there is no free-floating "default branch":
|
|
149
154
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
155
|
+
```
|
|
156
|
+
feature/x --base_map["*"]--> staging --base_map[staging]--> main
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
1. `base_map[<head>]` — the exact rung
|
|
160
|
+
2. `base_map["*"]` — ordinary work
|
|
161
|
+
3. stop and ask; never guess
|
|
154
162
|
|
|
155
|
-
|
|
163
|
+
A rung is a release edge: standing on `staging`, a PR targets `main` without
|
|
164
|
+
anyone passing `base:main`. Add a rung for any branch that releases elsewhere; a
|
|
165
|
+
three-rung ladder is just one more line. The map must be acyclic, and `"*"` must
|
|
166
|
+
exist — that is the rung everything falls back to.
|
|
156
167
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
168
|
+
The probed remote default is only useful as a hint for `"*"`: it tells you where
|
|
169
|
+
the repository considers work to land, not what every branch targets. Confirm it
|
|
170
|
+
before writing.
|
|
160
171
|
|
|
161
|
-
|
|
162
|
-
|
|
172
|
+
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.
|
|
163
176
|
|
|
164
177
|
**Branch name**, one per run. Derived once from the plan slug or topic before the
|
|
165
178
|
first commit, then passed to every repo. Multi-repo runs pass
|
|
@@ -180,7 +193,7 @@ recreate it. Never leave a PR aimed at the wrong base.
|
|
|
180
193
|
`origin` → `git.remote` → stop and say setup is incomplete.
|
|
181
194
|
|
|
182
195
|
**Mirror branch**: `SASSY_WORKSPACE_BRANCH` → the mirror's own
|
|
183
|
-
`refs/remotes/origin/HEAD` → `git.
|
|
196
|
+
`refs/remotes/origin/HEAD` → `git.mirror_branch` → `main`.
|
|
184
197
|
|
|
185
198
|
**Knowledge read.** Every path in `knowledge.read` that exists is searched for
|
|
186
199
|
prior learnings; a path that does not exist is skipped with one line. Absence of
|
|
@@ -188,13 +201,18 @@ matches is a fact worth stating, never evidence that no similar problem exists.
|
|
|
188
201
|
|
|
189
202
|
## Hooks
|
|
190
203
|
|
|
191
|
-
A project injects its own steps into
|
|
192
|
-
|
|
193
|
-
|
|
204
|
+
A project injects its own steps into the PR pipeline. They run by the skill that
|
|
205
|
+
owns the moment, so they fire whichever pipeline the user started. Nothing
|
|
206
|
+
declared means nothing runs.
|
|
194
207
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
208
|
+
**Keys are the head branch** — the branch the PR comes from, never the one it
|
|
209
|
+
targets. `hooks.pr.staging` means *PRs whose source is staging*.
|
|
210
|
+
|
|
211
|
+
| Key form | Matches |
|
|
212
|
+
|----------|---------|
|
|
213
|
+
| `staging` | that exact head |
|
|
214
|
+
| `release/*` | a family; only one trailing `*` is allowed |
|
|
215
|
+
| `"*"` | every other head |
|
|
198
216
|
|
|
199
217
|
| Point | Owner | Fires |
|
|
200
218
|
|-------|-------|-------|
|
|
@@ -203,25 +221,44 @@ directly.
|
|
|
203
221
|
| `before_merge` | `sassy-babysit` | the PR looks ready **and** this run will merge it |
|
|
204
222
|
| `after_merge` | `sassy-babysit` | this run observed the PR merged |
|
|
205
223
|
|
|
224
|
+
### Resolution
|
|
225
|
+
|
|
226
|
+
Per point, independently:
|
|
227
|
+
|
|
228
|
+
1. the exact head
|
|
229
|
+
2. the longest matching family, if the head starts with its prefix
|
|
230
|
+
3. `"*"`
|
|
231
|
+
|
|
232
|
+
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.
|
|
236
|
+
|
|
237
|
+
### Entries carry arguments
|
|
238
|
+
|
|
239
|
+
An entry is `<skill>` or `<skill> <args…>`, split on the first whitespace. Args
|
|
240
|
+
are passed to the hook verbatim — they are that skill's vocabulary, not ours. A
|
|
241
|
+
hook skill documents the args it accepts and must not silently ignore one.
|
|
242
|
+
|
|
243
|
+
Args are how a project pre-authorizes a step: `tap2-version mode:auto` runs
|
|
244
|
+
versioning without its approval prompt. Treat it like `pr.merge: true` — visible
|
|
245
|
+
in review, and commented when it is deliberate.
|
|
246
|
+
|
|
206
247
|
### Contract
|
|
207
248
|
|
|
208
249
|
- **Resolution**: `<repo>/.agents/skills/<name>/SKILL.md`, nearest walking up.
|
|
209
|
-
- **Names**: a skill the project owns, never one of ours.
|
|
210
|
-
prefix is the convention when the name could be confused with a general
|
|
211
|
-
workflow; `version` is fine when it is plainly the project's.
|
|
250
|
+
- **Names**: a skill the project owns, never one of ours.
|
|
212
251
|
- **Order**: declared order, one worker each, sequential.
|
|
213
252
|
- **Brief**: plan path, changed files, artifact paths, base, branch, commit hash,
|
|
214
253
|
and the PR URL where one exists.
|
|
215
|
-
- **Result**: `done`, `blocked`, or `needs-decision
|
|
216
|
-
orchestrator, which holds the gate
|
|
217
|
-
- **Fail closed**: a missing
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
them, because it runs after the pipeline's own commit; every other point must
|
|
221
|
-
leave the tree clean.
|
|
254
|
+
- **Result**: `done`, `blocked`, or `needs-decision`; a decision comes back to the
|
|
255
|
+
orchestrator, which holds the gate.
|
|
256
|
+
- **Fail closed**: a missing skill or a `blocked` result stops the pipeline.
|
|
257
|
+
- **Writes**: only `before_create` may also commit, because it runs after the
|
|
258
|
+
pipeline's commit; every other point leaves the tree clean.
|
|
222
259
|
- **Never**: push, open a PR, change `mode`, `branch`, or `merge`.
|
|
223
|
-
- **Idempotent**: `after_merge` can be reached by a later run observing an
|
|
224
|
-
merged PR, and `before_create` can run again on a re-ship.
|
|
260
|
+
- **Idempotent**: `after_merge` can be reached by a later run observing an
|
|
261
|
+
already merged PR, and `before_create` can run again on a re-ship.
|
|
225
262
|
- **Per PR**: in a workspace fan-out the hooks run once per PR.
|
|
226
263
|
|
|
227
264
|
### When they do not fire
|
|
@@ -10,12 +10,15 @@
|
|
|
10
10
|
set -euo pipefail
|
|
11
11
|
|
|
12
12
|
ROOT_ARG=""
|
|
13
|
+
HEAD_ARG=""
|
|
13
14
|
CHECK=0
|
|
14
15
|
|
|
15
16
|
while (( $# )); do
|
|
16
17
|
case "$1" in
|
|
17
18
|
--root) shift; [[ $# -ge 1 ]] || { echo "missing DIR after --root" >&2; exit 2; }; ROOT_ARG="$1" ;;
|
|
18
19
|
--root=*) ROOT_ARG="${1#*=}" ;;
|
|
20
|
+
--head) shift; [[ $# -ge 1 ]] || { echo "missing BRANCH after --head" >&2; exit 2; }; HEAD_ARG="$1" ;;
|
|
21
|
+
--head=*) HEAD_ARG="${1#*=}" ;;
|
|
19
22
|
--check) CHECK=1 ;;
|
|
20
23
|
-h|--help) sed -n '2,10p' "${BASH_SOURCE[0]}" | sed 's|^# \{0,1\}||'; exit 0 ;;
|
|
21
24
|
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
|
@@ -102,6 +105,91 @@ hook_skill_path() { # $1 hook skill name — nearest .agents/skills/<name>/SKILL
|
|
|
102
105
|
done
|
|
103
106
|
}
|
|
104
107
|
|
|
108
|
+
# --- config readers -------------------------------------------------------
|
|
109
|
+
# The file is flattened to `path<TAB>value` with paths joined by unit separators,
|
|
110
|
+
# which is enough to read the two nested maps we own (git.base_map, hooks.pr.*).
|
|
111
|
+
US=$'\037'
|
|
112
|
+
|
|
113
|
+
yaml_flat() {
|
|
114
|
+
awk -v us="$(printf '\037')" '
|
|
115
|
+
function ind(s, m) { m = match(s, /^ */); return m ? RLENGTH : 0 }
|
|
116
|
+
{
|
|
117
|
+
line = $0; sub(/#.*$/, "", line)
|
|
118
|
+
if (line ~ /^[[:space:]]*$/) next
|
|
119
|
+
d = ind(line)
|
|
120
|
+
body = line; sub(/^[[:space:]]*/, "", body); sub(/[[:space:]]+$/, "", body)
|
|
121
|
+
if (body ~ /:$/) {
|
|
122
|
+
key = body; sub(/:$/, "", key); gsub(/"/, "", key)
|
|
123
|
+
while (sp > 0 && sd[sp] >= d) sp--
|
|
124
|
+
sd[++sp] = d; sk[sp] = key
|
|
125
|
+
next
|
|
126
|
+
}
|
|
127
|
+
if (body !~ /:/) next
|
|
128
|
+
key = body; sub(/:.*/, "", key); gsub(/"/, "", key)
|
|
129
|
+
val = body; sub(/^[^:]*:[[:space:]]*/, "", val); gsub(/^"|"$/, "", val)
|
|
130
|
+
path = ""
|
|
131
|
+
for (i = 1; i <= sp; i++) if (sd[i] < d) path = path (path == "" ? "" : us) sk[i]
|
|
132
|
+
print path (path == "" ? "" : us) key "\t" val
|
|
133
|
+
}' "$CONFIG"
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
yflat() { yaml_flat | awk -F'\t' -v p="$1" '$1 == p { print $2; exit }'; }
|
|
137
|
+
|
|
138
|
+
list_items() {
|
|
139
|
+
printf '%s' "$1" | sed -e 's/^\[//' -e 's/\]$//' | tr ',' '\n' \
|
|
140
|
+
| sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//; s/^"//; s/"$//' | grep -v '^$' || true
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# map_match SECTION HEAD — exact key, then the longest matching family, then "*".
|
|
144
|
+
# base_map uses it, and hooks use the same rule, so "which pattern applies" has
|
|
145
|
+
# exactly one answer in the whole config.
|
|
146
|
+
map_match() {
|
|
147
|
+
local section="$1" head="$2" pat stem best="" bestlen=0 v
|
|
148
|
+
v="$(yflat "git${US}${section}${US}${head}")"
|
|
149
|
+
[[ -n "$v" ]] && { printf '%s' "$v"; return; }
|
|
150
|
+
while IFS= read -r pat; do
|
|
151
|
+
case "$pat" in *"*") ;; *) continue ;; esac
|
|
152
|
+
[[ "$pat" == "*" ]] && continue
|
|
153
|
+
stem="${pat%\*}"
|
|
154
|
+
if [[ "$head" == "$stem"* ]] && (( ${#stem} > bestlen )); then
|
|
155
|
+
best="$pat"; bestlen=${#stem}
|
|
156
|
+
fi
|
|
157
|
+
done < <(yaml_flat | awk -F'\t' -v us="$US" -v sec="$section" \
|
|
158
|
+
'{n=split($1,a,us); if (n==3 && a[1]=="git" && a[2]==sec) print a[3]}' | sort -u)
|
|
159
|
+
if [[ -n "$best" ]]; then
|
|
160
|
+
v="$(yflat "git${US}${section}${US}${best}")"
|
|
161
|
+
[[ -n "$v" ]] && { printf '%s' "$v"; return; }
|
|
162
|
+
fi
|
|
163
|
+
yflat "git${US}${section}${US}*"
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
base_for_head() { map_match base_map "$1"; }
|
|
167
|
+
|
|
168
|
+
hook_patterns() {
|
|
169
|
+
yaml_flat | awk -F'\t' -v us="$US" '{n=split($1,a,us); if (n==4 && a[1]=="hooks" && a[2]=="pr") print a[3]}' | sort -u
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# hooks_for_head_point HEAD POINT — exact head, then the longest matching
|
|
173
|
+
# family, then "*". A pattern that declares no entry for this point inherits.
|
|
174
|
+
hooks_for_head_point() {
|
|
175
|
+
local head="$1" point="$2" pat stem best="" bestlen=0 v
|
|
176
|
+
v="$(yflat "hooks${US}pr${US}${head}${US}${point}")"
|
|
177
|
+
[[ -n "$v" ]] && { printf '%s' "$v"; return; }
|
|
178
|
+
while IFS= read -r pat; do
|
|
179
|
+
case "$pat" in *"*") ;; *) continue ;; esac
|
|
180
|
+
[[ "$pat" == "*" ]] && continue
|
|
181
|
+
stem="${pat%\*}"
|
|
182
|
+
if [[ "$head" == "$stem"* ]] && (( ${#stem} > bestlen )); then
|
|
183
|
+
best="$pat"; bestlen=${#stem}
|
|
184
|
+
fi
|
|
185
|
+
done < <(hook_patterns)
|
|
186
|
+
if [[ -n "$best" ]]; then
|
|
187
|
+
v="$(yflat "hooks${US}pr${US}${best}${US}${point}")"
|
|
188
|
+
[[ -n "$v" ]] && { printf '%s' "$v"; return; }
|
|
189
|
+
fi
|
|
190
|
+
printf '%s' "$(yflat "hooks${US}pr${US}*${US}${point}")"
|
|
191
|
+
}
|
|
192
|
+
|
|
105
193
|
detect_base() {
|
|
106
194
|
local dir="$1" b
|
|
107
195
|
b="$(git -C "$dir" symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||' || true)"
|
|
@@ -156,7 +244,7 @@ case "$TOPOLOGY" in
|
|
|
156
244
|
;;
|
|
157
245
|
workspace)
|
|
158
246
|
MIRROR_REMOTE="$(config_value remote 2>/dev/null || true)"
|
|
159
|
-
MIRROR_BRANCH="$(config_value
|
|
247
|
+
MIRROR_BRANCH="$(config_value mirror_branch 2>/dev/null || true)"
|
|
160
248
|
MIRROR_BRANCH="${MIRROR_BRANCH:-main}"
|
|
161
249
|
;;
|
|
162
250
|
esac
|
|
@@ -193,18 +281,41 @@ if (( CHECK )); then
|
|
|
193
281
|
esac
|
|
194
282
|
fi
|
|
195
283
|
|
|
284
|
+
if [[ -n "$(yflat "git${US}default_branch")" ]]; then
|
|
285
|
+
if [[ "$TOPOLOGY" == "workspace" ]]; then
|
|
286
|
+
bad "git.default_branch is gone — the mirror's branch is git.mirror_branch"
|
|
287
|
+
else
|
|
288
|
+
bad "git.default_branch is gone — use git.base_map with a \"*\" entry"
|
|
289
|
+
fi
|
|
290
|
+
fi
|
|
291
|
+
|
|
196
292
|
if [[ "$TOPOLOGY" == "repo" ]]; then
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
293
|
+
if [[ -z "$(yflat "git${US}base_map${US}*")" ]]; then
|
|
294
|
+
bad "git.base_map has no \"*\" entry — it is the base for ordinary work"
|
|
295
|
+
else
|
|
296
|
+
while IFS= read -r pat; do
|
|
297
|
+
[[ -n "$pat" ]] || continue
|
|
298
|
+
case "$pat" in
|
|
299
|
+
"*") ;;
|
|
300
|
+
*"*") [[ "${pat%\*}" != *"*" ]] || bad "base_map key '$pat': only one trailing * is allowed" ;;
|
|
301
|
+
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)
|
|
303
|
+
head_ref="${HEAD_ARG:-$(git -C "$ROOT" branch --show-current 2>/dev/null || true)}"
|
|
304
|
+
if [[ -n "$head_ref" ]]; then
|
|
305
|
+
note " base for '${head_ref}': $(base_for_head "$head_ref")"
|
|
306
|
+
cur="$head_ref"; hops=0
|
|
307
|
+
while [[ -n "$cur" ]] && (( hops < 8 )); do
|
|
308
|
+
next="$(yflat "git${US}base_map${US}${cur}")"
|
|
309
|
+
[[ -z "$next" ]] && break
|
|
310
|
+
cur="$next"; hops=$((hops + 1))
|
|
311
|
+
if [[ "$cur" == "$head_ref" ]]; then
|
|
312
|
+
bad "git.base_map cycles through '$head_ref'"; break
|
|
313
|
+
fi
|
|
314
|
+
done
|
|
315
|
+
else
|
|
316
|
+
note " detached HEAD — pass --head <branch> to see the resolved base"
|
|
317
|
+
fi
|
|
205
318
|
fi
|
|
206
|
-
configured_remote="$(config_value remote 2>/dev/null || true)"
|
|
207
|
-
[[ -z "$configured_remote" ]] || bad "git.remote is set but git.repo is not workspace"
|
|
208
319
|
fi
|
|
209
320
|
|
|
210
321
|
if [[ "$TOPOLOGY" == "workspace" ]]; then
|
|
@@ -235,34 +346,40 @@ if (( CHECK )); then
|
|
|
235
346
|
done < <(nested_repos)
|
|
236
347
|
fi
|
|
237
348
|
|
|
238
|
-
# Hooks:
|
|
239
|
-
if
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
[[ -n "$
|
|
251
|
-
while IFS= read -r
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
case "$
|
|
255
|
-
sassy-*) bad "
|
|
256
|
-
*)
|
|
257
|
-
|
|
258
|
-
note " hook $hkey → $hname"
|
|
259
|
-
else
|
|
260
|
-
bad "hook $hkey: '$hname' not found at .agents/skills/$hname/SKILL.md"
|
|
261
|
-
fi
|
|
262
|
-
;;
|
|
349
|
+
# Hooks: head-scoped, so `staging` means "the PR is from staging".
|
|
350
|
+
if [[ -n "$(yflat "hooks${US}pr${US}before_create")" ]]; then
|
|
351
|
+
bad "hooks.pr.<point> is now nested by head branch — write hooks.pr.\"*\".<point>"
|
|
352
|
+
fi
|
|
353
|
+
while IFS= read -r pat; do
|
|
354
|
+
[[ -n "$pat" ]] || continue
|
|
355
|
+
case "$pat" in
|
|
356
|
+
"*") ;;
|
|
357
|
+
*"*") [[ "${pat%\*}" != *"*" ]] || bad "hook pattern '$pat': only one trailing * is allowed" ;;
|
|
358
|
+
esac
|
|
359
|
+
for point in before_create after_create before_merge after_merge; do
|
|
360
|
+
val="$(yflat "hooks${US}pr${US}${pat}${US}${point}")"
|
|
361
|
+
[[ -n "$val" ]] || continue
|
|
362
|
+
while IFS= read -r entry; do
|
|
363
|
+
[[ -n "$entry" ]] || continue
|
|
364
|
+
name="${entry%% *}"
|
|
365
|
+
case "$name" in
|
|
366
|
+
sassy-*) bad "hooks.$pat.$point: '$name' is one of our skills — hooks are project skills only" ;;
|
|
367
|
+
*) hook_skill_path "$name" >/dev/null 2>&1 \
|
|
368
|
+
|| bad "hooks.$pat.$point: '$name' not found at .agents/skills/$name/SKILL.md" ;;
|
|
263
369
|
esac
|
|
264
|
-
done < <(
|
|
265
|
-
done
|
|
370
|
+
done < <(list_items "$val")
|
|
371
|
+
done
|
|
372
|
+
done < <(hook_patterns)
|
|
373
|
+
|
|
374
|
+
# Resolve for one head, so nobody has to merge maps in their head.
|
|
375
|
+
if [[ -n "$HEAD_ARG" ]]; then
|
|
376
|
+
if [[ "$TOPOLOGY" == "repo" ]]; then
|
|
377
|
+
note " base for '$HEAD_ARG': $(base_for_head "$HEAD_ARG")"
|
|
378
|
+
fi
|
|
379
|
+
for point in before_create after_create before_merge after_merge; do
|
|
380
|
+
resolved="$(hooks_for_head_point "$HEAD_ARG" "$point")"
|
|
381
|
+
note " head '$HEAD_ARG' → $point: $( [[ -n "$resolved" ]] && printf '%s' "$resolved" || printf 'none' )"
|
|
382
|
+
done
|
|
266
383
|
fi
|
|
267
384
|
|
|
268
385
|
for key in plans solutions handbooks concepts; do
|
|
@@ -325,15 +442,18 @@ case "$TOPOLOGY" in
|
|
|
325
442
|
# --- Git ------------------------------------------------------------------
|
|
326
443
|
# repo: a remote name (this folder is the repo) | workspace (repos live below,
|
|
327
444
|
# this folder is not a repo) | none (no git anywhere here).
|
|
328
|
-
#
|
|
445
|
+
# base_map: the base for a HEAD branch, so a PR targets the right rung.
|
|
446
|
+
# "*" is ordinary work; add a rung for a branch that releases elsewhere.
|
|
329
447
|
git:
|
|
330
448
|
EOF
|
|
331
449
|
kv repo "$REPO_NAME" 0
|
|
450
|
+
printf ' base_map:\n'
|
|
332
451
|
if [[ -n "$DEFAULT_BRANCH" ]]; then
|
|
333
|
-
|
|
452
|
+
printf ' "*": %s # probed — confirm this is where ordinary work branches from\n' "$DEFAULT_BRANCH"
|
|
334
453
|
else
|
|
335
|
-
|
|
454
|
+
printf ' "*": %s # base for ordinary work; never guessed\n' "<confirm by hand>"
|
|
336
455
|
fi
|
|
456
|
+
printf ' # staging: main # uncomment when a branch releases somewhere else\n'
|
|
337
457
|
cat <<'EOF'
|
|
338
458
|
|
|
339
459
|
# workspace only: each nested repo folder -> the base its PRs target
|
|
@@ -347,12 +467,13 @@ EOF
|
|
|
347
467
|
# --- Git ------------------------------------------------------------------
|
|
348
468
|
# repo: workspace means repos live below and this folder is not a repo.
|
|
349
469
|
# remote: the snapshot mirror (GitHub owner/name or URL), required here.
|
|
350
|
-
#
|
|
470
|
+
# mirror_branch: the branch the mirror pushes to. Not a PR base — nested repos
|
|
471
|
+
# carry their own, and a workspace has no branches of its own.
|
|
351
472
|
git:
|
|
352
473
|
EOF
|
|
353
474
|
kv repo workspace 0
|
|
354
475
|
kv remote "${MIRROR_REMOTE:-<set the snapshot mirror, owner/name or URL>}" 0
|
|
355
|
-
kv
|
|
476
|
+
kv mirror_branch "$MIRROR_BRANCH" 0
|
|
356
477
|
cat <<'EOF'
|
|
357
478
|
|
|
358
479
|
# --- Repos ----------------------------------------------------------------
|
|
@@ -389,7 +510,7 @@ cat <<'EOF'
|
|
|
389
510
|
|
|
390
511
|
# --- Commit ---------------------------------------------------------------
|
|
391
512
|
# current: commit on the checked-out branch.
|
|
392
|
-
# new: cut a branch from
|
|
513
|
+
# new: cut a branch from the base map for the current head; name it in multi-repo runs so
|
|
393
514
|
# every repo gets the same branch name.
|
|
394
515
|
commit:
|
|
395
516
|
EOF
|
|
@@ -405,17 +526,22 @@ kv delete_branch false 24 '# true | false — delete the head branch once merged
|
|
|
405
526
|
cat <<'EOF'
|
|
406
527
|
|
|
407
528
|
# --- Hooks ----------------------------------------------------------------
|
|
408
|
-
# Project steps injected into the PR pipeline
|
|
409
|
-
#
|
|
410
|
-
#
|
|
411
|
-
#
|
|
412
|
-
#
|
|
413
|
-
# before_merge — this run is about to merge the PR
|
|
414
|
-
# after_merge — this run observed the PR merged
|
|
529
|
+
# Project steps injected into the PR pipeline, keyed by the HEAD branch — the
|
|
530
|
+
# branch the PR comes from, never the one it targets. `"*"` is the default;
|
|
531
|
+
# a name overrides per point, and `release/*` matches a family. Each entry is
|
|
532
|
+
# `<skill> <args…>`, resolved at .agents/skills/<name>/SKILL.md; args are passed
|
|
533
|
+
# to the skill verbatim. Declared order, one worker each, missing skill fails closed.
|
|
415
534
|
hooks:
|
|
416
535
|
pr:
|
|
536
|
+
"*":
|
|
537
|
+
EOF
|
|
538
|
+
kv before_create '[]' 30 '# after the commit, before the PR opens' ' '
|
|
539
|
+
kv after_create '[]' 30 '# the PR was just created' ' '
|
|
540
|
+
kv before_merge '[]' 30 '# this run is about to merge' ' '
|
|
541
|
+
kv after_merge '[]' 30 '# this run observed the merge' ' '
|
|
542
|
+
cat <<'EOF'
|
|
543
|
+
# staging: # per-point override for one head branch
|
|
544
|
+
# before_create: []
|
|
545
|
+
# release/*: # a family of heads
|
|
546
|
+
# after_merge: [notify mode:auto]
|
|
417
547
|
EOF
|
|
418
|
-
kv before_create '[]' 24 '# after the commit, before the PR opens' ' '
|
|
419
|
-
kv after_create '[]' 24 '# the PR was just created' ' '
|
|
420
|
-
kv before_merge '[]' 24 '# this run is about to merge' ' '
|
|
421
|
-
kv after_merge '[]' 24 '# this run observed the merge' ' '
|
|
@@ -6,7 +6,7 @@ description: >-
|
|
|
6
6
|
the rules and release steps that location requires. Use when asked to create a
|
|
7
7
|
skill, turn a workflow into a skill, fix a description or triggering,
|
|
8
8
|
restructure a bloated skill, or retune an existing one.
|
|
9
|
-
argument-hint: "[create|retune] [name, idea, or skill path]"
|
|
9
|
+
argument-hint: "[mode:create|retune|verify] [name, idea, or skill path]"
|
|
10
10
|
disable-model-invocation: true
|
|
11
11
|
---
|
|
12
12
|
|
|
@@ -14,6 +14,8 @@ disable-model-invocation: true
|
|
|
14
14
|
|
|
15
15
|
**Outcome:** a well-formed Agent Skill at a location the user chose, or an existing skill improved and re-verified. The harness loads it with no diagnostics.
|
|
16
16
|
|
|
17
|
+
**Modes.** `mode:create` writes a new skill. `mode:retune` changes the smallest thing that fixes a named failure in an existing skill. `mode:verify` checks an existing skill against the checklist and reports — it edits nothing. Omitted, detect it from the argument: something that already resolves to a skill directory is `retune`, or `verify` when the user only asked whether it is any good; anything else is `create`. State the mode you picked and why; when both are plausible, ask instead of guessing.
|
|
18
|
+
|
|
17
19
|
**Locations are not interchangeable** — they decide who gets the skill, how it ships, and what it may be called. Ask before writing.
|
|
18
20
|
|
|
19
21
|
**Scope:** skills only — not extensions, packages, or prompt templates.
|
|
@@ -54,7 +56,7 @@ Paths in the table are the defaults. Harnesses also read `skills` arrays in sett
|
|
|
54
56
|
|
|
55
57
|
## Workflow
|
|
56
58
|
|
|
57
|
-
1. **Intake.**
|
|
59
|
+
1. **Intake.** Settle the mode (above), then what the skill does, when it should trigger, and **where it lives** — internal, project, or user (see above). Ask; do not infer from the cwd. In `mode:retune`, read the existing skill and name the specific failure (won't trigger, too long, wrong steps, stale, wrong scope). In `mode:verify`, read it, run the checklist and Verify, report, and stop without writing.
|
|
58
60
|
2. **Choose the shape.** Single file, or `SKILL.md` + `references/` per the rule above.
|
|
59
61
|
3. **Write the frontmatter.** A description that states both what and when.
|
|
60
62
|
4. **Write the body.** Outcome, a hard-rules table, the workflow, and reference pointers. Plain English. Structured output where the skill returns a result. Match this setup's house style.
|
|
@@ -72,7 +74,7 @@ Paths in the table are the defaults. Harnesses also read `skills` arrays in sett
|
|
|
72
74
|
|
|
73
75
|
## Retune
|
|
74
76
|
|
|
75
|
-
|
|
77
|
+
In `mode:retune`, change the **smallest** thing that fixes the named failure:
|
|
76
78
|
|
|
77
79
|
- **Won't trigger** → rewrite the description (what + when, concrete verbs).
|
|
78
80
|
- **Too long / bloated** → extract conditional content to `references/`, or delete what never gets used.
|
|
@@ -83,6 +85,8 @@ Re-verify after any retune.
|
|
|
83
85
|
|
|
84
86
|
## Verify
|
|
85
87
|
|
|
88
|
+
Run this at the end of `create` and `retune`, and as the whole job in `mode:verify`.
|
|
89
|
+
|
|
86
90
|
- `/reload`.
|
|
87
91
|
- Check pi's startup output for warnings about the skill.
|
|
88
92
|
- Run `/skill:<name>` and confirm it loads and behaves.
|