@sassywares/skills 0.3.1 → 0.4.0
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 +16 -28
- package/package.json +3 -4
- package/skills/sassy-babysit/SKILL.md +4 -2
- package/skills/sassy-commit/SKILL.md +2 -2
- package/skills/sassy-commit-push-pr/SKILL.md +3 -2
- package/skills/sassy-debug/SKILL.md +0 -2
- package/skills/sassy-lfg/SKILL.md +0 -2
- package/skills/sassy-plan/SKILL.md +0 -2
- package/skills/sassy-review/SKILL.md +0 -2
- package/skills/sassy-setup/SKILL.md +9 -11
- package/skills/sassy-setup/assets/sassywares-readme.md +7 -5
- package/skills/sassy-setup/references/config.md +3 -3
- package/skills/sassy-setup/scripts/detect.sh +26 -6
- package/skills/sassy-ship/SKILL.md +2 -4
- package/skills/sassy-validate/SKILL.md +0 -2
- package/skills/sassy-work/SKILL.md +0 -2
- package/bin/sassy-skills.mjs +0 -193
- package/skills/sassy-plan/marker +0 -1
package/README.md
CHANGED
|
@@ -271,52 +271,40 @@ sassy-skills/
|
|
|
271
271
|
|
|
272
272
|
## Installation
|
|
273
273
|
|
|
274
|
-
The suite
|
|
275
|
-
|
|
276
|
-
### As a project dependency (recommended for teams)
|
|
274
|
+
The suite installs from this repository with the open `skills` CLI. That CLI vendors the skill files into your project and pins them, so a clone needs no network and every checkout runs the same skills.
|
|
277
275
|
|
|
278
276
|
```bash
|
|
279
|
-
|
|
277
|
+
npx skills add sassywares/sassy-skills
|
|
280
278
|
```
|
|
281
279
|
|
|
282
|
-
|
|
280
|
+
That writes three things:
|
|
283
281
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
ln -sfn "../../$d" ".agents/skills/$(basename "$d")"
|
|
288
|
-
done
|
|
289
|
-
```
|
|
282
|
+
- `.agents/skills/sassy-*` — the skill files themselves, read by pi and Cursor
|
|
283
|
+
- `.claude/skills/sassy-*` — links for Claude Code, which reads only its own directory
|
|
284
|
+
- `skills-lock.json` — source and content hash per skill
|
|
290
285
|
|
|
291
|
-
|
|
286
|
+
Commit the vendored skills and the lock; ignore `.claude/skills`, which is a link created at install time.
|
|
292
287
|
|
|
293
288
|
```bash
|
|
294
|
-
|
|
289
|
+
npx skills update -y # pull the latest from this repo
|
|
290
|
+
npx skills experimental_install # restore exactly what the lock pins
|
|
291
|
+
npx skills remove # take the suite out again
|
|
295
292
|
```
|
|
296
293
|
|
|
297
|
-
|
|
294
|
+
`sassy-setup` does all of this for you, including the ignore line and the two package scripts.
|
|
298
295
|
|
|
299
|
-
###
|
|
300
|
-
|
|
301
|
-
pi resolves packages directly, so no links are needed:
|
|
296
|
+
### While editing the suite
|
|
302
297
|
|
|
303
|
-
|
|
304
|
-
pi install npm:@sassywares/skills@0.1.0 # user-level, all projects
|
|
305
|
-
pi install npm:@sassywares/skills@0.1.0 --local # project-level → commit .pi/settings.json
|
|
306
|
-
```
|
|
298
|
+
`.pi/settings.json` in this repo declares `./skills`, so pi loads your working copy directly. Consumers see a change once it is pushed to `main` and they run `npx skills update` — no release step in between.
|
|
307
299
|
|
|
308
|
-
###
|
|
300
|
+
### Also published as a package
|
|
309
301
|
|
|
310
|
-
|
|
302
|
+
`@sassywares/skills` is on npm for harnesses that install packages rather than skill directories:
|
|
311
303
|
|
|
312
304
|
```bash
|
|
313
|
-
|
|
305
|
+
pi install npm:@sassywares/skills@0.4.0
|
|
314
306
|
```
|
|
315
307
|
|
|
316
|
-
Inside this repo, `.pi/settings.json` already declares `./skills`, so pi loads the working copy directly.
|
|
317
|
-
|
|
318
|
-
Get the skills however they arrive — package, link, or vendored copy — and invoke them with your harness's skill syntax.
|
|
319
|
-
|
|
320
308
|
## Repository maintenance
|
|
321
309
|
|
|
322
310
|
`scripts/sync.sh` commits skill changes, rebases on `origin/main`, and pushes. Any agent that changes a skill must run it immediately, without asking for confirmation. It installs nothing anywhere — a change reaches consumers when a version is released and they bump their pin. `AGENTS.md` documents the workflow for agents working on this repo.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sassywares/skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Lean, explicit-invocation Agent Skills for shipping fast: plan, work, review, validate, ship.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
],
|
|
25
25
|
"files": [
|
|
26
26
|
"skills",
|
|
27
|
-
"bin",
|
|
28
27
|
"README.md",
|
|
29
28
|
"CONCEPTS.md",
|
|
30
29
|
"LICENSE"
|
|
@@ -37,7 +36,7 @@
|
|
|
37
36
|
"./skills"
|
|
38
37
|
]
|
|
39
38
|
},
|
|
40
|
-
"
|
|
41
|
-
"
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
42
41
|
}
|
|
43
42
|
}
|
|
@@ -50,9 +50,11 @@ Stop when any holds:
|
|
|
50
50
|
- **Terminal** — merged or closed.
|
|
51
51
|
- **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
52
|
|
|
53
|
-
**Merge hooks.** Immediately before merging, run `hooks.pr.before_merge` in declared order — one `worker` per hook, brief
|
|
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.
|
|
54
54
|
|
|
55
|
-
**
|
|
55
|
+
**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
|
+
|
|
57
|
+
**Inert hooks.** With merging off, `before_merge` never fires; say so. Say so in one line — "not merging this run, merge hooks skipped" — rather than letting a declared hook vanish silently.
|
|
56
58
|
- **Blocked** — a `needs-human` item or a blocked-external dependency remains.
|
|
57
59
|
- **Budget** — the requested duration or a 3-day backstop has elapsed.
|
|
58
60
|
|
|
@@ -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 `git.default_branch`, else the probed base — 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 `repos[<dir>]` in a workspace config, else `git.default_branch`, 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
|
|
|
@@ -38,7 +38,7 @@ Treat the reads as a snapshot; re-read branch and staged set right before commit
|
|
|
38
38
|
|
|
39
39
|
0. **Gather** the context above.
|
|
40
40
|
1. **Nothing to commit** — if there are no staged, modified, or untracked files, say so and stop.
|
|
41
|
-
2. **Branch** — honor the `branch` option. With `branch:new`, cut from the base: `git fetch
|
|
41
|
+
2. **Branch** — honor the `branch` option. With `branch:new`, cut from the base: `git fetch <remote> <base>`, then `git checkout -b <name> <remote>/<base>`, then confirm `git merge-base HEAD <remote>/<base>` is `<remote>/<base>`. If that ancestry check fails, the branch was cut from the wrong base — recreate it, never push it. If the name exists, pick a non-conflicting suffix. With `branch:current` (default), commit on the checked-out branch; if that is the base branch or a detached HEAD, say so in one line before proceeding.
|
|
42
42
|
3. **Convention** — match the project's existing commit style; else the recent log pattern; else conventional commits (`type(scope): description`). When `fix`/`feat` both fit, default to `fix:`.
|
|
43
43
|
4. **Logical commits** — if changed files clearly split into distinct concerns, make separate commits (file level, 2–3 max, never `git add -p`). If ambiguous, one commit.
|
|
44
44
|
5. **Message** — imperative subject naming the outcome, not the file list. Body only when the motivation is not obvious.
|
|
@@ -28,7 +28,8 @@ git diff HEAD
|
|
|
28
28
|
git branch --show-current
|
|
29
29
|
git log --oneline -10
|
|
30
30
|
git rev-parse --abbrev-ref origin/HEAD
|
|
31
|
-
gh repo view --json defaultBranchRef --jq .defaultBranchRef.
|
|
31
|
+
gh repo view --json defaultBranchRef --jq .defaultBranchRef.name
|
|
32
|
+
gh pr view --json url,body,baseRefName,headRefName
|
|
32
33
|
gh pr list --head <branch> --state open --json url,number
|
|
33
34
|
```
|
|
34
35
|
|
|
@@ -36,7 +37,7 @@ Only an exit-0 empty list means "no open PR". A non-zero exit means **unknown**
|
|
|
36
37
|
|
|
37
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`.
|
|
38
39
|
|
|
39
|
-
**Base branch.** `<base>` is `repos[<repo dir>]` from a workspace config, else `git.default_branch`, else the probed remote HEAD
|
|
40
|
+
**Base branch.** `<base>` is `repos[<repo dir>]` from a workspace config, else `git.default_branch`, else the probed remote HEAD, **confirmed with the user before use**. 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>`.
|
|
40
41
|
|
|
41
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.
|
|
42
43
|
|
|
@@ -25,8 +25,6 @@ Trivial bugs whose cause is readable from the input may skip the scout, but stil
|
|
|
25
25
|
|
|
26
26
|
**Search prior learnings first.** Resolve `.sassywares/config.yaml` (nearest, walking up) per the `sassy-setup` skill's `references/config.md`. For every existing path in `knowledge.read`, search the docs for the symptom's keywords, the module, and the failure type. A matching doc **seeds or confirms** a hypothesis — it never replaces the causal chain or the reproduction. Name the docs you consulted, and say plainly when none matched: no match is not evidence that nothing similar has happened.
|
|
27
27
|
|
|
28
|
-
**Update nudge.** Once per run, if `node_modules/.bin/sassy-skills` exists, run `sassy-skills outdated`. It is throttled to once a day per machine and silently does nothing offline. Pass any newer version on in one line; never block, retry, or fail on it.
|
|
29
|
-
|
|
30
28
|
## Phase 1 — Diagnose inline, with `scout` as a helper
|
|
31
29
|
|
|
32
30
|
You own the diagnosis. The causal reasoning happens here, in the main context, because that is where the full picture lives and where the fix gate is presented.
|
|
@@ -16,8 +16,6 @@ disable-model-invocation: true
|
|
|
16
16
|
|
|
17
17
|
**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. Plans live under `artifacts.plans`; `git.repo` decides how the close-out persists.
|
|
18
18
|
|
|
19
|
-
**Update nudge.** Once per run, if `node_modules/.bin/sassy-skills` exists, run `sassy-skills outdated`. It is throttled to once a day per machine and silently does nothing offline. Pass any newer version on in one line; never block, retry, or fail on it.
|
|
20
|
-
|
|
21
19
|
## Input
|
|
22
20
|
|
|
23
21
|
Detect the shape from the argument:
|
|
@@ -17,8 +17,6 @@ disable-model-invocation: true
|
|
|
17
17
|
|
|
18
18
|
**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. Plans land in `artifacts.plans`.
|
|
19
19
|
|
|
20
|
-
**Update nudge.** Once per run, if `node_modules/.bin/sassy-skills` exists, run `sassy-skills outdated`. It is throttled to once a day per machine and silently does nothing offline. Pass any newer version on in one line; never block, retry, or fail on it.
|
|
21
|
-
|
|
22
20
|
## Phase A — Discuss (brainstorm)
|
|
23
21
|
|
|
24
22
|
Have one focused discussion to turn the idea into something plannable.
|
|
@@ -13,8 +13,6 @@ disable-model-invocation: true
|
|
|
13
13
|
|
|
14
14
|
**Outcome:** the scope reviewed with concrete findings; the approved fixes applied; everything left uncommitted.
|
|
15
15
|
|
|
16
|
-
**Update nudge.** Once per run, if `node_modules/.bin/sassy-skills` exists, run `sassy-skills outdated`. It is throttled to once a day per machine and silently does nothing offline. Pass any newer version on in one line; never block, retry, or fail on it.
|
|
17
|
-
|
|
18
16
|
**Orchestrator only.** Never edit code yourself. The `reviewer` finds; the `worker` fixes.
|
|
19
17
|
|
|
20
18
|
## Review dimensions
|
|
@@ -20,7 +20,7 @@ disable-model-invocation: true
|
|
|
20
20
|
## Steps
|
|
21
21
|
|
|
22
22
|
1. **Resolve the start.** The argument's path, else the working directory. If a `.sassywares/config.yaml` is already found walking up, this is a **verify run**: run the checks in step 7 and report, without rewriting anything.
|
|
23
|
-
2. **Detect.** Run `scripts/detect.sh --root <path>`. It prints a proposed config: topology (`repo`, `workspace`, `none`), the remote name, a probed base branch, nested repos with their bases
|
|
23
|
+
2. **Detect.** Run `scripts/detect.sh --root <path>`. It prints a proposed config: topology (`repo`, `workspace`, `none`), the remote name, a probed base branch, and nested repos with their bases.
|
|
24
24
|
3. **Ask only what detection cannot answer.** Never re-ask what the tree already said.
|
|
25
25
|
- **`workspace`** — the snapshot mirror: create a **private** GitHub repo, or connect an existing one? Creating it needs explicit approval and a named slug first.
|
|
26
26
|
- **`none`** — confirm the intent: this folder is deliberately outside git, so commit, PR, and babysit refuse here.
|
|
@@ -30,16 +30,14 @@ disable-model-invocation: true
|
|
|
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
31
|
- **Hooks** — offer, do not impose. Ask whether the project wants steps injected at `before_create`, `after_create`, `before_merge`, or `after_merge`, and write the skills they name. Leave them `[]` when they have 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
|
-
5. **Wire the skills.** The suite is the
|
|
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
|
|
|
35
|
-
- **
|
|
36
|
-
- **
|
|
37
|
-
- **
|
|
38
|
-
- **
|
|
39
|
-
- **Run `sassy-skills sync`.** The package's CLI is now in `node_modules/.bin`; it links `.agents/skills/sassy-*` and creates `.claude/skills → ../.agents/skills`. The linking rules travel with the lockfile, so they upgrade with the suite instead of drifting in the repo.
|
|
40
|
-
- **Ignore the derived links.** Add `/.agents/skills/sassy-*` and `/.claude/skills` to `.gitignore` when missing. Create `.gitignore` only when there is none.
|
|
35
|
+
- **Install it**: `npx -y skills add sassywares/sassy-skills -y`. That writes `.agents/skills/sassy-*` (real files), per-agent links such as `.claude/skills/sassy-*`, and `skills-lock.json`.
|
|
36
|
+
- **Scripts**, adding only what is missing: `"skills:sync": "npx -y skills experimental_install"` (restores exactly what the lock pins) and `"skills:update": "npx -y skills update -y"` (pulls the latest from upstream). **Never rewrite an existing `postinstall`** — leave it alone and tell the user to chain what they need.
|
|
37
|
+
- **Commit the vendored skills and the lock.** `.agents/skills/sassy-*` and `skills-lock.json` are the pin; a clone then needs no network.
|
|
38
|
+
- **Ignore `.claude/skills` only.** It is a link created at install time. Create `.gitignore` only when there is none — and in `workspace` topology never touch the project's root `.gitignore` at all.
|
|
41
39
|
- **Write `.sassywares/README.md`** from `assets/sassywares-readme.md` when absent. Never overwrite it — it may carry project notes.
|
|
42
|
-
- **Tell the user to `/reload`.** The
|
|
40
|
+
- **Tell the user to `/reload`.** The skills are invisible to the harness until it rescans.
|
|
43
41
|
|
|
44
42
|
6. **Install the snapshot, `workspace` only.** Read `references/workspace.md` and follow it.
|
|
45
43
|
|
|
@@ -55,9 +53,9 @@ disable-model-invocation: true
|
|
|
55
53
|
| No silent overwrite | An existing config or an existing `.sassywares/workspace/snapshot` is shown and asked about, never replaced blind. |
|
|
56
54
|
| Version is reported | Setup names the snapshot version it installed; `--check` flags an installed copy older than the skill's. |
|
|
57
55
|
| Never commit | Setup writes files and stops. Committing is `sassy-commit`. |
|
|
58
|
-
| Skills
|
|
56
|
+
| Skills come from upstream | Install with `npx skills add`; never hand-copy skill files or maintain the links yourself. |
|
|
59
57
|
| Scripts are shared | Adding a missing script is fine; rewriting an existing `postinstall` or `skills:*` entry is not. |
|
|
60
|
-
| Write boundary | Only `.sassywares/`, the `package.json` scripts and
|
|
58
|
+
| Write boundary | Only `.sassywares/`, the vendored `.agents/skills/sassy-*`, `skills-lock.json`, two `package.json` scripts, and one `.gitignore` line. Never a root `.gitignore` in a workspace. |
|
|
61
59
|
| Detect, don't guess | An unknown base branch stays unknown and gets asked about. A probed value is reported as probed. |
|
|
62
60
|
| Comments are part of the file | Every key keeps its description and allowed values. A stripped config is an incomplete config. |
|
|
63
61
|
| Hooks are named, not written | Setup records hook skill names. Creating those skills is the project's work. |
|
|
@@ -4,14 +4,16 @@ Project config and tooling for the Sassy Skills suite. Written by `sassy-setup`.
|
|
|
4
4
|
|
|
5
5
|
## Skills
|
|
6
6
|
|
|
7
|
-
Installed
|
|
7
|
+
Installed from upstream with the open `skills` CLI, which vendors the skills into
|
|
8
|
+
`.agents/skills/` and pins them in `skills-lock.json`:
|
|
8
9
|
|
|
9
10
|
```bash
|
|
10
|
-
|
|
11
|
-
bun run skills:
|
|
11
|
+
npx skills add sassywares/sassy-skills # first install
|
|
12
|
+
bun run skills:sync # restore exactly what the lock pins
|
|
13
|
+
bun run skills:update # pull the latest from upstream
|
|
12
14
|
```
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
The vendored skills and the lock are committed; `.claude/skills` is a link and is not.
|
|
15
17
|
|
|
16
18
|
`.agents/skills/` is read by pi and Cursor; Claude Code reads `.claude/skills/` only, which is why that symlink exists. After any change, `/reload` your harness.
|
|
17
19
|
|
|
@@ -22,7 +24,7 @@ If a skill tells you another skill is missing, run `skills:sync` and restart. Su
|
|
|
22
24
|
`config.yaml` holds the project's paths, git topology, and PR defaults. Every key is documented inline, including its allowed values, and the full schema is in the `sassy-setup` skill's `references/config.md`. Verify the file still matches the tree:
|
|
23
25
|
|
|
24
26
|
```bash
|
|
25
|
-
|
|
27
|
+
.agents/skills/sassy-setup/scripts/detect.sh --root . --check
|
|
26
28
|
```
|
|
27
29
|
|
|
28
30
|
## Hooks
|
|
@@ -168,9 +168,9 @@ first commit, then passed to every repo. Multi-repo runs pass
|
|
|
168
168
|
Cut from the base, then verify:
|
|
169
169
|
|
|
170
170
|
```
|
|
171
|
-
git fetch
|
|
172
|
-
git checkout -b <name>
|
|
173
|
-
git merge-base HEAD
|
|
171
|
+
git fetch <remote> <base>
|
|
172
|
+
git checkout -b <name> <remote>/<base>
|
|
173
|
+
git merge-base HEAD <remote>/<base> # must be <remote>/<base>
|
|
174
174
|
```
|
|
175
175
|
|
|
176
176
|
An ancestry check that fails means the branch was cut from the wrong base —
|
|
@@ -25,6 +25,8 @@ done
|
|
|
25
25
|
|
|
26
26
|
START="${ROOT_ARG:-$PWD}"
|
|
27
27
|
[[ -d "$START" ]] || { echo "not a directory: $START" >&2; exit 2; }
|
|
28
|
+
# Absolute, or the walk-up loop below re-reads "." forever.
|
|
29
|
+
START="$(cd "$START" && pwd -P)"
|
|
28
30
|
|
|
29
31
|
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
30
32
|
ASSET_SNAPSHOT="$SKILL_DIR/assets/snapshot"
|
|
@@ -38,8 +40,24 @@ while :; do
|
|
|
38
40
|
probe="$(dirname "$probe")"
|
|
39
41
|
done
|
|
40
42
|
|
|
43
|
+
# A --check run validates the config against the tree the CONFIG describes, which
|
|
44
|
+
# is not the git toplevel when you are standing inside one of its nested repos.
|
|
45
|
+
if [[ -n "$CONFIG" ]]; then
|
|
46
|
+
ROOT="${CONFIG%/.sassywares/config.yaml}"
|
|
47
|
+
if git -C "$ROOT" rev-parse --show-toplevel >/dev/null 2>&1; then
|
|
48
|
+
TOPOLOGY="repo"
|
|
49
|
+
else
|
|
50
|
+
TOPOLOGY="workspace"
|
|
51
|
+
shopt -s nullglob
|
|
52
|
+
NESTED_FOUND=0
|
|
53
|
+
for child in "$ROOT"/*/; do
|
|
54
|
+
[[ -d "$child/.git" ]] && { NESTED_FOUND=1; break; }
|
|
55
|
+
done
|
|
56
|
+
shopt -u nullglob
|
|
57
|
+
[[ "$NESTED_FOUND" -eq 1 ]] || TOPOLOGY="none"
|
|
58
|
+
fi
|
|
41
59
|
# Topology root: the git toplevel when inside a work tree, else the start dir.
|
|
42
|
-
|
|
60
|
+
elif TOPLEVEL="$(git -C "$START" rev-parse --show-toplevel 2>/dev/null)"; then
|
|
43
61
|
ROOT="$TOPLEVEL"
|
|
44
62
|
TOPOLOGY="repo"
|
|
45
63
|
else
|
|
@@ -96,8 +114,10 @@ detect_base() {
|
|
|
96
114
|
}
|
|
97
115
|
|
|
98
116
|
remote_name() {
|
|
99
|
-
local dir="$1"
|
|
100
|
-
git -C "$dir" remote 2>/dev/null
|
|
117
|
+
local dir="$1" remotes
|
|
118
|
+
remotes="$(git -C "$dir" remote 2>/dev/null)"
|
|
119
|
+
if printf '%s\n' "$remotes" | grep -qx origin; then printf 'origin'; return 0; fi
|
|
120
|
+
printf '%s\n' "$remotes" | head -n 1
|
|
101
121
|
}
|
|
102
122
|
|
|
103
123
|
nested_repos() {
|
|
@@ -131,7 +151,8 @@ case "$TOPOLOGY" in
|
|
|
131
151
|
repo)
|
|
132
152
|
REPO_NAME="$(remote_name "$ROOT")"
|
|
133
153
|
REPO_NAME="${REPO_NAME:-origin}"
|
|
134
|
-
|
|
154
|
+
# A check run reads the base from the config; probing costs a network call.
|
|
155
|
+
(( CHECK )) || DEFAULT_BRANCH="$(detect_base "$ROOT")"
|
|
135
156
|
;;
|
|
136
157
|
workspace)
|
|
137
158
|
MIRROR_REMOTE="$(config_value remote 2>/dev/null || true)"
|
|
@@ -174,14 +195,13 @@ if (( CHECK )); then
|
|
|
174
195
|
|
|
175
196
|
if [[ "$TOPOLOGY" == "repo" ]]; then
|
|
176
197
|
configured_base="$(config_value default_branch 2>/dev/null || true)"
|
|
198
|
+
[[ -n "$configured_base" ]] || DEFAULT_BRANCH="$(detect_base "$ROOT")"
|
|
177
199
|
if [[ -z "$configured_base" ]]; then
|
|
178
200
|
bad "git.default_branch is unset — name the branch new work is cut from"
|
|
179
201
|
elif [[ -n "$DEFAULT_BRANCH" && "$configured_base" != "$DEFAULT_BRANCH" ]]; then
|
|
180
202
|
# A team base is a policy choice: staging, dev, or the release branch can
|
|
181
203
|
# all differ from the remote default. Report it, do not call it drift.
|
|
182
204
|
note " default_branch '$configured_base' overrides the remote default '$DEFAULT_BRANCH' — expected when PRs target a team base"
|
|
183
|
-
elif [[ -z "$DEFAULT_BRANCH" ]]; then
|
|
184
|
-
note " remote default undetectable — '$configured_base' taken as declared"
|
|
185
205
|
fi
|
|
186
206
|
configured_remote="$(config_value remote 2>/dev/null || true)"
|
|
187
207
|
[[ -z "$configured_remote" ]] || bad "git.remote is set but git.repo is not workspace"
|
|
@@ -6,7 +6,7 @@ description: >-
|
|
|
6
6
|
delegated to subagents. Default `mode:manual` presents a shipping summary and
|
|
7
7
|
waits before anything is written, committed, pushed, or opened. `mode:auto`
|
|
8
8
|
runs the close-out with no review.
|
|
9
|
-
argument-hint: "<plan path or topic> [steps:compound,handbook,commit,pr,babysit] [merge:on|off] [delete-branch:on|off] [mode:manual|auto]"
|
|
9
|
+
argument-hint: "<plan path or topic> [steps:compound,handbook,commit,pr,babysit] [base:<branch>] [merge:on|off] [delete-branch:on|off] [mode:manual|auto]"
|
|
10
10
|
disable-model-invocation: true
|
|
11
11
|
---
|
|
12
12
|
|
|
@@ -18,8 +18,6 @@ disable-model-invocation: true
|
|
|
18
18
|
|
|
19
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. `pr.babysit`, `pr.merge`, and `pr.delete_branch` seed the close-out; `git.repo` decides whether this run commits, snapshots, or refuses; `hooks.pr.*` adds project steps to the PR pipeline.
|
|
20
20
|
|
|
21
|
-
**Update nudge.** Once per run, if `node_modules/.bin/sassy-skills` exists, run `sassy-skills outdated`. It is throttled to once a day per machine and silently does nothing offline. Pass any newer version on in one line; never block, retry, or fail on it.
|
|
22
|
-
|
|
23
21
|
**Subagent-maximal by design.** The orchestrator delegates every heavy step (docs, tree reads, git, gh) to subagents and only receives compact summaries. It never reads diffs, file contents, or the tree itself.
|
|
24
22
|
|
|
25
23
|
## Steps (in order)
|
|
@@ -31,7 +29,7 @@ disable-model-invocation: true
|
|
|
31
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.
|
|
32
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`.
|
|
33
31
|
- **`git.repo: none`.** No commit exists here. Report that the work is not persisted and stop.
|
|
34
|
-
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
|
|
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.
|
|
35
33
|
|
|
36
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.
|
|
37
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.
|
|
@@ -13,8 +13,6 @@ disable-model-invocation: true
|
|
|
13
13
|
|
|
14
14
|
**Outcome:** a grounded conformance judgment against an intent; the approved gaps closed; everything left uncommitted.
|
|
15
15
|
|
|
16
|
-
**Update nudge.** Once per run, if `node_modules/.bin/sassy-skills` exists, run `sassy-skills outdated`. It is throttled to once a day per machine and silently does nothing offline. Pass any newer version on in one line; never block, retry, or fail on it.
|
|
17
|
-
|
|
18
16
|
**Orchestrator only.** Never edit code yourself. The `validator` finds gaps; the `worker` closes them.
|
|
19
17
|
|
|
20
18
|
**This skill needs an intent.** Validation without something to validate against is meaningless. If the user gave no plan path or intent, ask for one before doing anything.
|
|
@@ -19,8 +19,6 @@ disable-model-invocation: true
|
|
|
19
19
|
|
|
20
20
|
**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. `git.repo` decides what the phase commit does, `artifacts.plans` decides where the plan lives.
|
|
21
21
|
|
|
22
|
-
**Update nudge.** Once per run, if `node_modules/.bin/sassy-skills` exists, run `sassy-skills outdated`. It is throttled to once a day per machine and silently does nothing offline. Pass any newer version on in one line; never block, retry, or fail on it.
|
|
23
|
-
|
|
24
22
|
## Input
|
|
25
23
|
|
|
26
24
|
Detect the shape from the argument:
|
package/bin/sassy-skills.mjs
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// sassy-skills — wire this package's skills into a project.
|
|
3
|
-
//
|
|
4
|
-
// Nothing scans node_modules on its own, so the harnesses that read a project
|
|
5
|
-
// skills directory need links. This tool creates them, checks them, and updates
|
|
6
|
-
// the pinned version.
|
|
7
|
-
//
|
|
8
|
-
// sassy-skills sync link skills into .agents/skills (and .claude/skills)
|
|
9
|
-
// sassy-skills check exit 1 if the wiring is missing or stale
|
|
10
|
-
// sassy-skills update bump to the latest published version, then sync
|
|
11
|
-
//
|
|
12
|
-
// Options: --root <dir> --quiet --help
|
|
13
|
-
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
14
|
-
import { tmpdir } from "node:os";
|
|
15
|
-
import { execFileSync } from "node:child_process";
|
|
16
|
-
import { dirname, join, resolve } from "node:path";
|
|
17
|
-
import { fileURLToPath } from "node:url";
|
|
18
|
-
|
|
19
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
20
|
-
const SKILLS_DIR = resolve(HERE, "..", "skills");
|
|
21
|
-
const PKG = JSON.parse(readFileSync(resolve(HERE, "..", "package.json"), "utf8"));
|
|
22
|
-
const NAME = PKG.name;
|
|
23
|
-
const VERSION = PKG.version;
|
|
24
|
-
const REGISTRY = process.env.SASSY_SKILLS_REGISTRY ?? "https://registry.npmjs.org";
|
|
25
|
-
|
|
26
|
-
const args = process.argv.slice(2);
|
|
27
|
-
const quiet = args.includes("--quiet");
|
|
28
|
-
const rootFlag = args.indexOf("--root");
|
|
29
|
-
const rootValueIndex = rootFlag >= 0 ? rootFlag + 1 : -1;
|
|
30
|
-
const ROOT = rootFlag >= 0 ? resolve(args[rootFlag + 1] ?? ".") : process.cwd();
|
|
31
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
32
|
-
console.log(`sassy-skills@${VERSION}\n\n sync link skills into .agents/skills and .claude/skills\n check exit 1 when the wiring is missing or stale\n update bump ${NAME} to the latest version, then sync\n outdated print a one-line nudge when a newer version is published (throttled, offline-safe)\n\nOptions: --root <dir> --quiet --force`);
|
|
33
|
-
process.exit(0);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const command = args.find((a, i) => !a.startsWith("-") && i !== rootValueIndex) ?? "sync";
|
|
37
|
-
|
|
38
|
-
const log = (...a) => { if (!quiet) console.log(...a); };
|
|
39
|
-
const ok = (m) => log(` ✓ ${m}`);
|
|
40
|
-
const warn = (m) => console.error(` ! ${m}`);
|
|
41
|
-
const die = (m) => { console.error(` ✗ ${m}`); process.exit(1); };
|
|
42
|
-
|
|
43
|
-
const IS_WIN = process.platform === "win32";
|
|
44
|
-
|
|
45
|
-
function linkedSkills() {
|
|
46
|
-
if (!existsSync(SKILLS_DIR)) die(`package has no skills/ directory at ${SKILLS_DIR}`);
|
|
47
|
-
return readdirSync(SKILLS_DIR, { withFileTypes: true })
|
|
48
|
-
.filter((e) => e.isDirectory() && existsSync(join(SKILLS_DIR, e.name, "SKILL.md")))
|
|
49
|
-
.map((e) => e.name)
|
|
50
|
-
.sort();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function makeLink(target, linkPath) {
|
|
54
|
-
try {
|
|
55
|
-
if (lstatSync(linkPath).isSymbolicLink()) rmSync(linkPath);
|
|
56
|
-
else { warn(`${linkPath} exists and is not a symlink — leaving it alone`); return false; }
|
|
57
|
-
} catch { /* does not exist */ }
|
|
58
|
-
symlinkSync(target, linkPath, IS_WIN ? "junction" : "dir");
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function sync() {
|
|
63
|
-
const skills = linkedSkills();
|
|
64
|
-
const agents = join(ROOT, ".agents", "skills");
|
|
65
|
-
mkdirSync(agents, { recursive: true });
|
|
66
|
-
|
|
67
|
-
let made = 0;
|
|
68
|
-
for (const name of skills) {
|
|
69
|
-
const target = resolve(SKILLS_DIR, name);
|
|
70
|
-
if (makeLink(target, join(agents, name))) made++;
|
|
71
|
-
}
|
|
72
|
-
ok(`${skills.length} skills linked into .agents/skills (${NAME}@${VERSION})`);
|
|
73
|
-
|
|
74
|
-
// Claude Code reads .claude/skills only. A directory symlink is enough, and it
|
|
75
|
-
// is the least invasive form when the directory does not already exist.
|
|
76
|
-
const claude = join(ROOT, ".claude", "skills");
|
|
77
|
-
try {
|
|
78
|
-
const st = lstatSync(claude);
|
|
79
|
-
if (!st.isSymbolicLink()) {
|
|
80
|
-
warn(".claude/skills exists and is not a symlink — Claude Code will not see the project skills unless you link them yourself");
|
|
81
|
-
}
|
|
82
|
-
} catch {
|
|
83
|
-
mkdirSync(dirname(claude), { recursive: true });
|
|
84
|
-
symlinkSync(IS_WIN ? join(ROOT, ".agents", "skills") : "../.agents/skills", claude, IS_WIN ? "junction" : "dir");
|
|
85
|
-
ok("linked .claude/skills → .agents/skills");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (made) log(`\nRestart or /reload your harness to pick up the new skills.`);
|
|
89
|
-
return 0;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function check() {
|
|
93
|
-
const skills = linkedSkills();
|
|
94
|
-
const agents = join(ROOT, ".agents", "skills");
|
|
95
|
-
let problems = 0;
|
|
96
|
-
|
|
97
|
-
for (const name of skills) {
|
|
98
|
-
const link = join(agents, name);
|
|
99
|
-
const target = resolve(SKILLS_DIR, name);
|
|
100
|
-
try {
|
|
101
|
-
const st = lstatSync(link);
|
|
102
|
-
if (!st.isSymbolicLink()) { warn(`${name}: real directory, not a link`); problems++; continue; }
|
|
103
|
-
const points = resolve(agents, readlinkSync(link));
|
|
104
|
-
if (points !== target) { warn(`${name}: points at ${points}, expected ${target}`); problems++; }
|
|
105
|
-
} catch {
|
|
106
|
-
warn(`${name}: not linked`);
|
|
107
|
-
problems++;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
if (!existsSync(join(ROOT, ".claude", "skills"))) warn(".claude/skills missing (Claude Code only)");
|
|
111
|
-
if (problems) { console.error(` ✗ ${problems} skill link(s) need attention — run: sassy-skills sync`); return 1; }
|
|
112
|
-
ok(`${skills.length} skills correctly linked (${NAME}@${VERSION})`);
|
|
113
|
-
return 0;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function detectPM() {
|
|
117
|
-
if (existsSync(join(ROOT, "bun.lock")) || existsSync(join(ROOT, "bun.lockb"))) return "bun";
|
|
118
|
-
if (existsSync(join(ROOT, "pnpm-lock.yaml"))) return "pnpm";
|
|
119
|
-
if (existsSync(join(ROOT, "yarn.lock"))) return "yarn";
|
|
120
|
-
if (existsSync(join(ROOT, "package-lock.json"))) return "npm";
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function update() {
|
|
125
|
-
const pm = detectPM();
|
|
126
|
-
if (!pm) die("no lockfile found — cannot tell which package manager to update with");
|
|
127
|
-
const spec = `${NAME}@latest`;
|
|
128
|
-
const cmd = {
|
|
129
|
-
bun: ["bun", ["add", "--dev", "--exact", spec]],
|
|
130
|
-
npm: ["npm", ["install", "--save-dev", "--save-exact", spec]],
|
|
131
|
-
pnpm: ["pnpm", ["add", "--save-dev", "--save-exact", spec]],
|
|
132
|
-
yarn: ["yarn", ["add", "--dev", "--exact", spec]],
|
|
133
|
-
}[pm];
|
|
134
|
-
log(`Updating ${NAME} with ${pm}…`);
|
|
135
|
-
execFileSync(cmd[0], cmd[1], { cwd: ROOT, stdio: "inherit" });
|
|
136
|
-
return sync();
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function newerThan(a, b) {
|
|
140
|
-
const pa = String(a).split(/[.+-]/).map((x) => parseInt(x, 10) || 0);
|
|
141
|
-
const pb = String(b).split(/[.+-]/).map((x) => parseInt(x, 10) || 0);
|
|
142
|
-
for (let i = 0; i < 3; i++) {
|
|
143
|
-
if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) > (pb[i] ?? 0);
|
|
144
|
-
}
|
|
145
|
-
return false;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function updateHint() {
|
|
149
|
-
const pm = detectPM();
|
|
150
|
-
const script = pm === "bun" ? "bun run skills:update"
|
|
151
|
-
: pm ? `${pm} run skills:update`
|
|
152
|
-
: "sassy-skills update";
|
|
153
|
-
return script;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Non-blocking version nudge. Throttled to once a day per machine, silent when
|
|
157
|
-
// offline, and it never fails a run — a stale pin is worth mentioning, not
|
|
158
|
-
// worth blocking work over.
|
|
159
|
-
async function outdated(force = false) {
|
|
160
|
-
const cacheFile = join(tmpdir(), "sassy-skills-version-check.json");
|
|
161
|
-
const TTL = 24 * 60 * 60 * 1000;
|
|
162
|
-
let cached = null;
|
|
163
|
-
try { cached = JSON.parse(readFileSync(cacheFile, "utf8")); } catch { /* first run */ }
|
|
164
|
-
|
|
165
|
-
let latest = null;
|
|
166
|
-
const fresh = cached && typeof cached.checkedAt === "number" && Date.now() - cached.checkedAt < TTL;
|
|
167
|
-
if (fresh && !force) {
|
|
168
|
-
latest = cached.latest ?? null;
|
|
169
|
-
} else {
|
|
170
|
-
try {
|
|
171
|
-
const res = await fetch(`${REGISTRY}/${encodeURIComponent(NAME)}/latest`, {
|
|
172
|
-
signal: AbortSignal.timeout(2000),
|
|
173
|
-
headers: { accept: "application/json" },
|
|
174
|
-
});
|
|
175
|
-
const body = await res.json();
|
|
176
|
-
latest = typeof body?.version === "string" ? body.version : null;
|
|
177
|
-
} catch { latest = null; }
|
|
178
|
-
try { writeFileSync(cacheFile, JSON.stringify({ checkedAt: Date.now(), latest })); } catch { /* read-only tmp */ }
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (!latest || !newerThan(latest, VERSION)) return 0;
|
|
182
|
-
console.log(` ↑ ${NAME} ${VERSION} → ${latest} available — ${updateHint()}`);
|
|
183
|
-
return 0;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
switch (command) {
|
|
187
|
-
case "outdated": process.exit(await outdated(args.includes("--force")));
|
|
188
|
-
case "sync": process.exit(sync());
|
|
189
|
-
case "check": process.exit(check());
|
|
190
|
-
case "update": process.exit(update());
|
|
191
|
-
default:
|
|
192
|
-
die(`unknown command: ${command} (try --help)`);
|
|
193
|
-
}
|
package/skills/sassy-plan/marker
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
keep
|