@sassywares/skills 0.3.0 → 0.3.2
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 +7 -14
- package/bin/sassy-skills.mjs +70 -13
- package/package.json +4 -1
- 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 +1 -1
- package/skills/sassy-lfg/SKILL.md +1 -1
- package/skills/sassy-plan/SKILL.md +1 -1
- package/skills/sassy-review/SKILL.md +1 -1
- package/skills/sassy-setup/SKILL.md +3 -3
- package/skills/sassy-setup/references/config.md +3 -3
- package/skills/sassy-setup/scripts/detect.sh +26 -6
- package/skills/sassy-ship/SKILL.md +3 -3
- package/skills/sassy-skill/SKILL.md +26 -8
- package/skills/sassy-validate/SKILL.md +1 -1
- package/skills/sassy-work/SKILL.md +1 -1
- package/skills/sassy-plan/marker +0 -1
package/README.md
CHANGED
|
@@ -276,33 +276,26 @@ The suite ships as the npm package **`@sassywares/skills`**. Pin it, don't symli
|
|
|
276
276
|
### As a project dependency (recommended for teams)
|
|
277
277
|
|
|
278
278
|
```bash
|
|
279
|
-
bun add
|
|
279
|
+
bun add --dev --exact @sassywares/skills@0.3.2
|
|
280
280
|
```
|
|
281
281
|
|
|
282
|
-
Nothing scans `node_modules` on its own, so
|
|
282
|
+
Nothing scans `node_modules` on its own, so link it where harnesses look — the package does that itself:
|
|
283
283
|
|
|
284
284
|
```bash
|
|
285
|
-
|
|
286
|
-
for d in node_modules/@sassywares/skills/skills/*/; do
|
|
287
|
-
ln -sfn "../../$d" ".agents/skills/$(basename "$d")"
|
|
288
|
-
done
|
|
285
|
+
bunx sassy-skills sync
|
|
289
286
|
```
|
|
290
287
|
|
|
291
|
-
`.agents/skills
|
|
288
|
+
That writes `.agents/skills/sassy-*` (read by pi and Cursor) and the `.claude/skills` link Claude Code needs. Add it as `"postinstall": "sassy-skills sync"` and teammates need nothing beyond `bun install`.
|
|
292
289
|
|
|
293
|
-
|
|
294
|
-
mkdir -p .claude && ln -sfn ../.agents/skills .claude/skills
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
Ignore both: they're derived from the lockfile, like any build output. A `postinstall` that runs the loop gives teammates zero setup beyond `bun install`; a preflight in the skills tells anyone who skipped it what to run.
|
|
290
|
+
Ignore both paths — they're derived from the lockfile, like any build output. `sassy-setup` does all of this for you, including the gitignore lines and the pinned version.
|
|
298
291
|
|
|
299
292
|
### As a pi package
|
|
300
293
|
|
|
301
294
|
pi resolves packages directly, so no links are needed:
|
|
302
295
|
|
|
303
296
|
```bash
|
|
304
|
-
pi install npm:@sassywares/skills@0.
|
|
305
|
-
pi install npm:@sassywares/skills@0.
|
|
297
|
+
pi install npm:@sassywares/skills@0.3.2 # user-level, all projects
|
|
298
|
+
pi install npm:@sassywares/skills@0.3.2 --local # project-level → commit .pi/settings.json
|
|
306
299
|
```
|
|
307
300
|
|
|
308
301
|
### While editing the suite
|
package/bin/sassy-skills.mjs
CHANGED
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
// sassy-skills sync link skills into .agents/skills (and .claude/skills)
|
|
9
9
|
// sassy-skills check exit 1 if the wiring is missing or stale
|
|
10
10
|
// sassy-skills update bump to the latest published version, then sync
|
|
11
|
+
// sassy-skills outdated one-line nudge when a newer version is published
|
|
11
12
|
//
|
|
12
|
-
// Options: --root <dir> --quiet --help
|
|
13
|
-
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
13
|
+
// Options: --root <dir> --quiet --force --help
|
|
14
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
14
15
|
import { tmpdir } from "node:os";
|
|
15
16
|
import { execFileSync } from "node:child_process";
|
|
16
17
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -27,7 +28,17 @@ const args = process.argv.slice(2);
|
|
|
27
28
|
const quiet = args.includes("--quiet");
|
|
28
29
|
const rootFlag = args.indexOf("--root");
|
|
29
30
|
const rootValueIndex = rootFlag >= 0 ? rootFlag + 1 : -1;
|
|
30
|
-
const
|
|
31
|
+
const rootArg = rootFlag >= 0 ? args[rootFlag + 1] : undefined;
|
|
32
|
+
if (rootFlag >= 0 && (!rootArg || rootArg.startsWith("-"))) {
|
|
33
|
+
console.error(" ✗ --root needs a directory");
|
|
34
|
+
process.exit(2);
|
|
35
|
+
}
|
|
36
|
+
const ROOT = rootArg ? resolve(rootArg) : process.cwd();
|
|
37
|
+
if (rootFlag >= 0) {
|
|
38
|
+
let ok = false;
|
|
39
|
+
try { ok = statSync(ROOT).isDirectory(); } catch { ok = false; }
|
|
40
|
+
if (!ok) { console.error(` ✗ --root ${ROOT} is not an existing directory`); process.exit(2); }
|
|
41
|
+
}
|
|
31
42
|
if (args.includes("--help") || args.includes("-h")) {
|
|
32
43
|
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
44
|
process.exit(0);
|
|
@@ -50,13 +61,22 @@ function linkedSkills() {
|
|
|
50
61
|
.sort();
|
|
51
62
|
}
|
|
52
63
|
|
|
64
|
+
// Returns "linked", "kept", or "blocked". A blocked path is a real directory the
|
|
65
|
+
// user owns: never replaced, and always a failure, because the wiring is then
|
|
66
|
+
// incomplete and nothing the tool can do will fix it.
|
|
53
67
|
function makeLink(target, linkPath) {
|
|
54
68
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
69
|
+
const st = lstatSync(linkPath);
|
|
70
|
+
if (st.isSymbolicLink()) {
|
|
71
|
+
if (resolve(dirname(linkPath), readlinkSync(linkPath)) === target) return "kept";
|
|
72
|
+
rmSync(linkPath);
|
|
73
|
+
} else {
|
|
74
|
+
warn(`${linkPath} is a real directory, not a link — move it aside; nothing was replaced`);
|
|
75
|
+
return "blocked";
|
|
76
|
+
}
|
|
57
77
|
} catch { /* does not exist */ }
|
|
58
78
|
symlinkSync(target, linkPath, IS_WIN ? "junction" : "dir");
|
|
59
|
-
return
|
|
79
|
+
return "linked";
|
|
60
80
|
}
|
|
61
81
|
|
|
62
82
|
function sync() {
|
|
@@ -64,12 +84,21 @@ function sync() {
|
|
|
64
84
|
const agents = join(ROOT, ".agents", "skills");
|
|
65
85
|
mkdirSync(agents, { recursive: true });
|
|
66
86
|
|
|
67
|
-
let
|
|
87
|
+
let linked = 0;
|
|
88
|
+
let blocked = 0;
|
|
68
89
|
for (const name of skills) {
|
|
69
90
|
const target = resolve(SKILLS_DIR, name);
|
|
70
|
-
|
|
91
|
+
const result = makeLink(target, join(agents, name));
|
|
92
|
+
if (result === "linked") linked++;
|
|
93
|
+
else if (result === "blocked") blocked++;
|
|
94
|
+
}
|
|
95
|
+
if (blocked) {
|
|
96
|
+
console.error(` ✗ ${blocked} of ${skills.length} skills could not be linked — see above`);
|
|
97
|
+
return 1;
|
|
71
98
|
}
|
|
72
|
-
ok(
|
|
99
|
+
ok(linked
|
|
100
|
+
? `${skills.length} skills linked into .agents/skills (${NAME}@${VERSION})`
|
|
101
|
+
: `${skills.length} skills already linked (${NAME}@${VERSION})`);
|
|
73
102
|
|
|
74
103
|
// Claude Code reads .claude/skills only. A directory symlink is enough, and it
|
|
75
104
|
// is the least invasive form when the directory does not already exist.
|
|
@@ -77,7 +106,13 @@ function sync() {
|
|
|
77
106
|
try {
|
|
78
107
|
const st = lstatSync(claude);
|
|
79
108
|
if (!st.isSymbolicLink()) {
|
|
80
|
-
warn(".claude/skills
|
|
109
|
+
warn(".claude/skills is a real directory — Claude Code will not see the project skills until you link them");
|
|
110
|
+
return 1;
|
|
111
|
+
}
|
|
112
|
+
const points = resolve(dirname(claude), readlinkSync(claude));
|
|
113
|
+
if (points !== resolve(agents)) {
|
|
114
|
+
warn(`.claude/skills points at ${points}, not .agents/skills — relink it`);
|
|
115
|
+
return 1;
|
|
81
116
|
}
|
|
82
117
|
} catch {
|
|
83
118
|
mkdirSync(dirname(claude), { recursive: true });
|
|
@@ -85,7 +120,7 @@ function sync() {
|
|
|
85
120
|
ok("linked .claude/skills → .agents/skills");
|
|
86
121
|
}
|
|
87
122
|
|
|
88
|
-
if (
|
|
123
|
+
if (linked) log(`\nRestart or /reload your harness to pick up the new skills.`);
|
|
89
124
|
return 0;
|
|
90
125
|
}
|
|
91
126
|
|
|
@@ -107,7 +142,14 @@ function check() {
|
|
|
107
142
|
problems++;
|
|
108
143
|
}
|
|
109
144
|
}
|
|
110
|
-
|
|
145
|
+
const claude = join(ROOT, ".claude", "skills");
|
|
146
|
+
try {
|
|
147
|
+
const st = lstatSync(claude);
|
|
148
|
+
if (!st.isSymbolicLink()) { warn(".claude/skills is a real directory, so Claude Code sees none of these"); problems++; }
|
|
149
|
+
else if (resolve(dirname(claude), readlinkSync(claude)) !== resolve(agents)) {
|
|
150
|
+
warn(".claude/skills points somewhere else"); problems++;
|
|
151
|
+
}
|
|
152
|
+
} catch { warn(".claude/skills missing (Claude Code only)"); }
|
|
111
153
|
if (problems) { console.error(` ✗ ${problems} skill link(s) need attention — run: sassy-skills sync`); return 1; }
|
|
112
154
|
ok(`${skills.length} skills correctly linked (${NAME}@${VERSION})`);
|
|
113
155
|
return 0;
|
|
@@ -132,7 +174,22 @@ function update() {
|
|
|
132
174
|
yarn: ["yarn", ["add", "--dev", "--exact", spec]],
|
|
133
175
|
}[pm];
|
|
134
176
|
log(`Updating ${NAME} with ${pm}…`);
|
|
135
|
-
|
|
177
|
+
try {
|
|
178
|
+
execFileSync(cmd[0], cmd[1], { cwd: ROOT, stdio: "inherit" });
|
|
179
|
+
} catch {
|
|
180
|
+
die(`${pm} could not update ${NAME} — nothing was relinked`);
|
|
181
|
+
}
|
|
182
|
+
// The running copy is the old version; its skills directory may be gone. Hand
|
|
183
|
+
// over to the freshly installed binary so the linker matches the new package.
|
|
184
|
+
const fresh = join(ROOT, "node_modules", ".bin", "sassy-skills");
|
|
185
|
+
if (existsSync(fresh)) {
|
|
186
|
+
try {
|
|
187
|
+
execFileSync(fresh, ["sync", "--root", ROOT], { stdio: "inherit" });
|
|
188
|
+
} catch {
|
|
189
|
+
die("updated, but the new binary could not relink — run: sassy-skills sync");
|
|
190
|
+
}
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
136
193
|
return sync();
|
|
137
194
|
}
|
|
138
195
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sassywares/skills",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Lean, explicit-invocation Agent Skills for shipping fast: plan, work, review, validate, ship.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,5 +39,8 @@
|
|
|
39
39
|
},
|
|
40
40
|
"bin": {
|
|
41
41
|
"sassy-skills": "./bin/sassy-skills.mjs"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18"
|
|
42
45
|
}
|
|
43
46
|
}
|
|
@@ -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,7 +25,7 @@ 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.
|
|
28
|
+
**Update nudge.** Once per run, if `node_modules/.bin/sassy-skills` exists, run `node_modules/.bin/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
29
|
|
|
30
30
|
## Phase 1 — Diagnose inline, with `scout` as a helper
|
|
31
31
|
|
|
@@ -16,7 +16,7 @@ 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.
|
|
19
|
+
**Update nudge.** Once per run, if `node_modules/.bin/sassy-skills` exists, run `node_modules/.bin/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
20
|
|
|
21
21
|
## Input
|
|
22
22
|
|
|
@@ -17,7 +17,7 @@ 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.
|
|
20
|
+
**Update nudge.** Once per run, if `node_modules/.bin/sassy-skills` exists, run `node_modules/.bin/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
21
|
|
|
22
22
|
## Phase A — Discuss (brainstorm)
|
|
23
23
|
|
|
@@ -13,7 +13,7 @@ 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.
|
|
16
|
+
**Update nudge.** Once per run, if `node_modules/.bin/sassy-skills` exists, run `node_modules/.bin/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
17
|
|
|
18
18
|
**Orchestrator only.** Never edit code yourself. The `reviewer` finds; the `worker` fixes.
|
|
19
19
|
|
|
@@ -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.
|
|
@@ -37,7 +37,7 @@ disable-model-invocation: true
|
|
|
37
37
|
- **Add the dependency pinned exactly** — `bun add --dev --exact @sassywares/skills@<version>`, `npm install --save-dev --save-exact …`, `pnpm add --save-dev --save-exact …`, `yarn add --dev --exact …`. Exact, because one pinned version for every teammate and every harness is the entire point.
|
|
38
38
|
- **Scripts**, adding only what is missing: `"skills:sync": "sassy-skills sync"`, `"skills:update": "sassy-skills update"`, and `"postinstall": "sassy-skills sync"`. **Never overwrite an existing `postinstall`** — if one is there, leave it alone and tell the user to chain `&& sassy-skills sync`, so setup cannot erase someone's install steps.
|
|
39
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
|
|
40
|
+
- **Ignore the derived links — `repo` topology only.** Add `/.agents/skills/sassy-*` and `/.claude/skills` to `.gitignore` when missing, creating it only when there is none. In `workspace` topology never touch the project's root `.gitignore`; the snapshot's own ignore file covers that case.
|
|
41
41
|
- **Write `.sassywares/README.md`** from `assets/sassywares-readme.md` when absent. Never overwrite it — it may carry project notes.
|
|
42
42
|
- **Tell the user to `/reload`.** The links are invisible to the harness until it rescans.
|
|
43
43
|
|
|
@@ -57,7 +57,7 @@ disable-model-invocation: true
|
|
|
57
57
|
| Never commit | Setup writes files and stops. Committing is `sassy-commit`. |
|
|
58
58
|
| Skills are a dependency | Pin `@sassywares/skills`; never copy skill files into a project or maintain hand-made links. |
|
|
59
59
|
| 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 devDependency, the lockfile, and two `.gitignore` lines. Never skill content. |
|
|
60
|
+
| Write boundary | Only `.sassywares/`, the `package.json` scripts and devDependency, the lockfile, and — in `repo` topology — two `.gitignore` lines. Never skill content, and never a root `.gitignore` in a workspace. |
|
|
61
61
|
| Detect, don't guess | An unknown base branch stays unknown and gets asked about. A probed value is reported as probed. |
|
|
62
62
|
| Comments are part of the file | Every key keeps its description and allowed values. A stripped config is an incomplete config. |
|
|
63
63
|
| Hooks are named, not written | Setup records hook skill names. Creating those skills is the project's work. |
|
|
@@ -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,7 +18,7 @@ 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.
|
|
21
|
+
**Update nudge.** Once per run, if `node_modules/.bin/sassy-skills` exists, run `node_modules/.bin/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
22
|
|
|
23
23
|
**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
24
|
|
|
@@ -31,7 +31,7 @@ disable-model-invocation: true
|
|
|
31
31
|
- **`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
32
|
- **`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
33
|
- **`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
|
|
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`, 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
35
|
|
|
36
36
|
**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
37
|
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.
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sassy-skill
|
|
3
3
|
description: >-
|
|
4
|
-
Create, retune, or improve an Agent Skill
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
Create, retune, or improve an Agent Skill. Asks where the skill should live —
|
|
5
|
+
inside this suite, in a project, or for one user — then writes it there with
|
|
6
|
+
the rules and release steps that location requires. Use when asked to create a
|
|
7
|
+
skill, turn a workflow into a skill, fix a description or triggering,
|
|
8
|
+
restructure a bloated skill, or retune an existing one.
|
|
8
9
|
argument-hint: "[create|retune] [name, idea, or skill path]"
|
|
9
10
|
disable-model-invocation: true
|
|
10
11
|
---
|
|
11
12
|
|
|
12
13
|
# Sassy Skill
|
|
13
14
|
|
|
14
|
-
**Outcome:** a well-formed Agent Skill
|
|
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
|
+
|
|
17
|
+
**Locations are not interchangeable** — they decide who gets the skill, how it ships, and what it may be called. Ask before writing.
|
|
15
18
|
|
|
16
19
|
**Scope:** skills only — not extensions, packages, or prompt templates.
|
|
17
20
|
|
|
@@ -22,11 +25,26 @@ disable-model-invocation: true
|
|
|
22
25
|
- **Description:** required, ≤1024 chars. Pi advertises name + description at startup; the body loads only when the skill is used. The description decides whether the skill ever fires — write **what it does and when to use it**.
|
|
23
26
|
- **Frontmatter fields:** `name`, `description` (required); optional `license`, `compatibility`, `metadata`, `allowed-tools`, `disable-model-invocation`.
|
|
24
27
|
- Use `disable-model-invocation: true` for skills that should run only via explicit `/skill:name`.
|
|
25
|
-
- **Locations:** user `~/.agents/skills/<name>/`; project `<repo>/.agents/skills/<name>/` (discovered from cwd up to the repo root). Also the `skills` array in settings and pi packages.
|
|
26
28
|
- **Reference other skills by name, never by absolute path.** The suite can live in `~/.agents/skills/`, a project `.agents/skills/`, or inside `node_modules/`, so "load the sassy-commit skill" is portable where a `~/…` path is not. Name a bundled file as "the sassy-setup skill's `references/config.md`".
|
|
27
29
|
- Pi **validates at startup**; malformed or descriptionless skills warn. `/reload` applies changes.
|
|
28
30
|
- Pi implements the **Agent Skills specification** (agentskills.io). For the full spec, pi's own `docs/skills.md` is authoritative — read it when the rules here are not enough.
|
|
29
31
|
|
|
32
|
+
## Where the skill lives
|
|
33
|
+
|
|
34
|
+
Ask which one, unless the request already made it obvious. The answer changes the name, the sharing, and whether a release is needed.
|
|
35
|
+
|
|
36
|
+
| Location | Path | Who gets it | Extra work |
|
|
37
|
+
|---|---|---|---|
|
|
38
|
+
| **Internal** — part of this suite | `skills/<name>/` in the sassy-skills repo | Everyone, after a release | Name it `sassy-<name>`, bump `package.json`, run `scripts/sync.sh`, then tag to publish |
|
|
39
|
+
| **Project** — team workflow | `<repo>/.agents/skills/<name>/` | Everyone who clones that repo | Commit it; prefix the project (`tap2-…`) so it can't collide with a suite skill or a harness built-in |
|
|
40
|
+
| **User** — personal workflow | `~/.agents/skills/<name>/` | Only this person, every repo on this machine | None — but nothing gates it either; never put something a teammate needs here |
|
|
41
|
+
|
|
42
|
+
Recommend by audience: the team needs it → **project**; only the requester → **user**; it belongs to every project using this suite → **internal**.
|
|
43
|
+
|
|
44
|
+
Internal is the only one that implies a release. Never choose it silently: from the moment it lands, consumers see it only when they bump their pin. Say that out loud when you do.
|
|
45
|
+
|
|
46
|
+
Paths in the table are the defaults. Harnesses also read `skills` arrays in settings and installed packages, so a skill may live elsewhere — write down where it actually went.
|
|
47
|
+
|
|
30
48
|
## Shape rule — the lean rule
|
|
31
49
|
|
|
32
50
|
- **Default to one `SKILL.md`.**
|
|
@@ -36,7 +54,7 @@ disable-model-invocation: true
|
|
|
36
54
|
|
|
37
55
|
## Workflow
|
|
38
56
|
|
|
39
|
-
1. **Intake.** What the skill does, when it should trigger, and
|
|
57
|
+
1. **Intake.** 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. For a retune, read the existing skill and name the specific failure (won't trigger, too long, wrong steps, stale, wrong scope).
|
|
40
58
|
2. **Choose the shape.** Single file, or `SKILL.md` + `references/` per the rule above.
|
|
41
59
|
3. **Write the frontmatter.** A description that states both what and when.
|
|
42
60
|
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.
|
|
@@ -59,7 +77,7 @@ When asked to retune or improve a skill, change the **smallest** thing that fixe
|
|
|
59
77
|
- **Won't trigger** → rewrite the description (what + when, concrete verbs).
|
|
60
78
|
- **Too long / bloated** → extract conditional content to `references/`, or delete what never gets used.
|
|
61
79
|
- **Wrong or stale steps** → correct the workflow against the current tree.
|
|
62
|
-
- **Wrong scope** → move it between
|
|
80
|
+
- **Wrong scope** → move it between internal, project, and user. Moving into the suite is a release; moving out of it means the next release drops the skill for everyone.
|
|
63
81
|
|
|
64
82
|
Re-verify after any retune.
|
|
65
83
|
|
|
@@ -13,7 +13,7 @@ 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.
|
|
16
|
+
**Update nudge.** Once per run, if `node_modules/.bin/sassy-skills` exists, run `node_modules/.bin/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
17
|
|
|
18
18
|
**Orchestrator only.** Never edit code yourself. The `validator` finds gaps; the `worker` closes them.
|
|
19
19
|
|
|
@@ -19,7 +19,7 @@ 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.
|
|
22
|
+
**Update nudge.** Once per run, if `node_modules/.bin/sassy-skills` exists, run `node_modules/.bin/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
23
|
|
|
24
24
|
## Input
|
|
25
25
|
|
package/skills/sassy-plan/marker
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
keep
|