@sassywares/skills 0.5.13 → 0.5.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sassywares/skills",
3
- "version": "0.5.13",
3
+ "version": "0.5.14",
4
4
  "description": "Lean, explicit-invocation Agent Skills for shipping fast: plan, work, review, validate, ship.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -52,7 +52,7 @@ A check run never edits the config and never rewrites the tree: it prints what `
52
52
  - **Write `.sassywares/README.md`** from `assets/sassywares-readme.md` when absent. Never overwrite it — it may carry project notes.
53
53
  - **Tell the user to `/reload`.** The skills are invisible to the harness until it rescans.
54
54
 
55
- 6. **Install the snapshot, `workspace` only.** Read `references/workspace.md` and follow it.
55
+ 6. **Install the workspace tooling, `workspace` only.** The snapshot plus the `branches` / `pull` / `checkout-bases` git helpers. Read `references/workspace.md` and follow it.
56
56
  7. **Verify.** Run `scripts/detect.sh --root <path> --check`. It validates the config against the tree — topology and remotes, `base_map` shape and cycles, `repos` against each nested repo, hooks, artifacts paths, `knowledge`, `pr.*` booleans, `commit.branch`, any unknown key, and in `workspace` mode the installed snapshot's version — plus the vendored pin: every `.agents/skills/sassy-*` folder hashed against `skills-lock.json`, the lock's coverage both ways, and whether git carries the lock. Report every `drift:` line verbatim, or that the config matches the tree. Drift blocks step 8.
57
57
  8. **Commit what setup owns.** `repo` topology only, and only when step 7 is clean. The file list is exactly what setup wrote or found unwritten:
58
58
 
@@ -65,7 +65,7 @@ A check run never edits the config and never rewrites the tree: it prints what `
65
65
  - **`workspace`** — nothing to commit: the root is not a repo, and `references/workspace.md` forbids running the snapshot here. Say the root config and the pin land with the project's next snapshot.
66
66
  - **`none`** — nothing to commit, deliberately.
67
67
 
68
- 9. **Report.** The config path, the topology, the resolved artifact and knowledge paths, the commit (hash and branch) or why there is none, and in `workspace` mode what was installed. Remind: `/reload` so the harness rescans. Close with what is left over — at most dirt setup deliberately did not commit — and nothing else: a clean setup leaves the user good to go.
68
+ 9. **Report.** The config path, the topology, the resolved artifact and knowledge paths, the commit (hash and branch) or why there is none, and in `workspace` mode what was installed, with versions (`snapshot vN`, `workspace-git vN`). Remind: `/reload` so the harness rescans. Close with what is left over — at most dirt setup deliberately did not commit — and nothing else: a clean setup leaves the user good to go.
69
69
 
70
70
  ## Hard rules
71
71
 
@@ -76,10 +76,10 @@ A check run never edits the config and never rewrites the tree: it prints what `
76
76
  | Verify before commit | A config that fails `--check` is reported, never committed. |
77
77
  | Approval to create a remote | Creating the mirror repo needs an explicit yes and a named slug first. |
78
78
  | No silent overwrite | An existing config or an existing `.sassywares/workspace/snapshot` is shown and asked about, never replaced blind. |
79
- | Version is reported | Setup names the snapshot version it installed; `--check` flags an installed copy older than the skill's. |
79
+ | Version is reported | Setup names the snapshot and `workspace-git` versions it installed; `--check` flags an installed copy older than the skill's. |
80
80
  | Skills come from upstream | Install with `npx skills add`; never hand-copy skill files or maintain the links yourself. |
81
81
  | Never edit the manifest | No dependencies, no scripts, no `postinstall`. Setup installs the suite; the project decides how it updates. |
82
- | Write boundary | Only `.sassywares/`, the vendored `.agents/skills/sassy-*`, `skills-lock.json`, and one `.gitignore` line — and the same list is step 8's commit. Never `package.json`, never a root `.gitignore` in a workspace. |
82
+ | Write boundary | Only `.sassywares/`, the vendored `.agents/skills/sassy-*`, `skills-lock.json`, and one `.gitignore` line — and the same list is step 8's commit. Never `package.json`, never a root `.gitignore` in a workspace. Workspace git helpers land in `.sassywares/workspace/`, never in a project's `bin/`. |
83
83
  | Detect, don't guess | An unknown base branch stays unknown and gets asked about. A probed value is reported as probed. |
84
84
  | Comments are part of the file | Every key keeps its description and allowed values. A stripped config is an incomplete config. |
85
85
  | Hooks are named, not written | Setup records hook skill names. Creating those skills is the project's work. |
@@ -31,6 +31,23 @@ If a skill tells you another skill is missing, run `npx skills add sassywares/sa
31
31
 
32
32
  Or ask for `mode:check` on the setup skill, which runs the same check and reports it. Neither moves the pin: `npx skills check` is an alias of `update`.
33
33
 
34
+ ## Workspace scripts
35
+
36
+ In a `workspace` project, `.sassywares/workspace/` holds the scripts that work
37
+ across the sibling repos, next to the `snapshot` that versions the glue:
38
+
39
+ ```bash
40
+ .sassywares/workspace/branches # branch + dirty marker per repo
41
+ .sassywares/workspace/pull # fetch, then ff-only per repo
42
+ .sassywares/workspace/checkout-bases # put each repo on its base
43
+ .sassywares/workspace/snapshot # commit + push the glue
44
+ ```
45
+
46
+ They read the repo inventory and the PR bases from `repos:` in `config.yaml`, so
47
+ there is no second base map to keep in step. They are this project's files:
48
+ extend them freely — just run the snapshot afterwards, since they live in the
49
+ mirrored tree. The `sassy-*` skills do their own git and never call them.
50
+
34
51
  ## Hooks
35
52
 
36
53
  `hooks.pr` names project skills that run inside the PR pipeline, keyed by the PR's **head** branch: `"*"` for every PR, an exact branch, or a family such as `release/*`, overriding per point (`before_create`, `after_create`, `before_merge`, `after_merge`). An entry is `<skill> <args…>`; the args mean whatever that skill says they mean — `mode:auto` pre-authorizes the step. Hooks are ordinary skills in `.agents/skills/`, which makes them **code**: review them like code, and never point a hook at a copy of a suite skill.
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env bash
2
+ # Shared helpers for the workspace git scripts: branches, pull, checkout-bases.
3
+ #
4
+ # Installed next to those scripts by sassy-setup, and owned by this project from
5
+ # then on — extend it here rather than forking a copy. The one rule worth keeping
6
+ # is that the repo inventory and the PR bases come from `repos:` in
7
+ # .sassywares/config.yaml; never grow a second base map in this file.
8
+ #
9
+ # workspace-git-version: 1
10
+
11
+ set -euo pipefail
12
+
13
+ WORKSPACE_GIT_VERSION="1"
14
+
15
+ WS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
+ ROOT="$(cd "$WS_DIR/../.." && pwd)"
17
+ CONFIG="$ROOT/.sassywares/config.yaml"
18
+ # Nested repos conventionally push to origin; override for a workspace that does not.
19
+ REMOTE="${WORKSPACE_GIT_REMOTE:-origin}"
20
+
21
+ log() { printf '%s\n' "$*"; }
22
+ ok() { printf ' ✓ %s\n' "$*"; }
23
+ warn() { printf ' ! %s\n' "$*" >&2; }
24
+ die() { printf ' ✗ %s\n' "$*" >&2; exit 1; }
25
+
26
+ need_git() {
27
+ command -v git >/dev/null 2>&1 || die "git not found"
28
+ }
29
+
30
+ [[ -f "$CONFIG" ]] || die "no config at $CONFIG — run sassy-setup"
31
+
32
+ # Print `<dir><TAB><base>` for every entry under `repos:`, in file order.
33
+ # Deliberately shallow: that one map, not a YAML parser.
34
+ workspace_repos() {
35
+ awk '
36
+ /^[[:space:]]*#/ { next }
37
+ /^repos:[[:space:]]*$/ { in_repos = 1; next }
38
+ !in_repos { next }
39
+ /^[^[:space:]]/ { exit }
40
+ /^[[:space:]]*$/ { next }
41
+ {
42
+ line = $0
43
+ sub(/^[[:space:]]+/, "", line)
44
+ dir = line; sub(/:.*/, "", dir)
45
+ base = line; sub(/^[^:]*:[[:space:]]*/, "", base); sub(/[[:space:]]+#.*$/, "", base)
46
+ if (dir != "" && base != "") printf "%s\t%s\n", dir, base
47
+ }
48
+ ' "$CONFIG"
49
+ }
50
+
51
+ # Immediate children that are their own git repo but carry no `repos:` entry.
52
+ # A drift signal for `branches`: never a base inferred on the project's behalf.
53
+ unmapped_repos() {
54
+ local path name mapped
55
+ mapped="$(workspace_repos | cut -f1)"
56
+ for path in "$ROOT"/*/; do
57
+ name="$(basename "$path")"
58
+ [[ "$name" == node_modules || "$name" == .* ]] && continue
59
+ [[ -d "$path/.git" ]] || continue
60
+ if ! printf '%s\n' "$mapped" | grep -Fxq -- "$name"; then
61
+ printf '%s\n' "$name"
62
+ fi
63
+ done
64
+ }
65
+
66
+ repo_has_git() {
67
+ [[ -d "$1/.git" ]]
68
+ }
69
+
70
+ current_branch() {
71
+ git -C "$1" rev-parse --abbrev-ref HEAD 2>/dev/null
72
+ }
73
+
74
+ is_dirty() {
75
+ [[ -n "$(git -C "$1" status --porcelain 2>/dev/null)" ]]
76
+ }
77
+
78
+ short_head() {
79
+ git -C "$1" rev-parse --short HEAD 2>/dev/null
80
+ }
81
+
82
+ # The most useful line of captured git output: its first `error:`/`fatal:` line
83
+ # when there is one, else the last non-blank line. Git's advice blocks are noise
84
+ # in a one-line warn.
85
+ git_err() {
86
+ local line
87
+ line="$(printf '%s\n' "$1" | grep -m1 -E '^(error|fatal):' || true)"
88
+ if [[ -n "$line" ]]; then
89
+ printf '%s\n' "$line"
90
+ return 0
91
+ fi
92
+ printf '%s\n' "$1" | grep -v '^[[:space:]]*$' | tail -n 1
93
+ }
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env bash
2
+ # Show the current branch (and a dirty marker) for every repo in `repos:`.
3
+ #
4
+ # Repos that exist on disk but have no `repos:` entry are listed as unmapped —
5
+ # add them to .sassywares/config.yaml rather than letting a base be guessed.
6
+ #
7
+ # Usage: .sassywares/workspace/branches [--version]
8
+ set -euo pipefail
9
+
10
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ # shellcheck source=./_lib.sh
12
+ source "$SCRIPT_DIR/_lib.sh"
13
+
14
+ usage() {
15
+ cat <<EOF
16
+ Usage: .sassywares/workspace/branches [--version]
17
+
18
+ Print the current branch for each repo in the config's \`repos:\` map, with a
19
+ trailing * when the tree is dirty and \`(base: …)\` when it is not on its base.
20
+
21
+ Repos without a \`repos:\` entry are listed as unmapped; setup fills that map in.
22
+ EOF
23
+ }
24
+
25
+ case "${1:-}" in
26
+ -h|--help) usage; exit 0 ;;
27
+ --version) printf '%s\n' "$WORKSPACE_GIT_VERSION"; exit 0 ;;
28
+ "") ;;
29
+ *) die "unknown argument: $1 (try --help)" ;;
30
+ esac
31
+
32
+ need_git
33
+
34
+ found=0
35
+ while IFS=$'\t' read -r name base; do
36
+ [[ -n "$name" ]] || continue
37
+ path="$ROOT/$name"
38
+ if ! repo_has_git "$path"; then
39
+ printf '%-22s (missing)\n' "$name"
40
+ continue
41
+ fi
42
+ found=1
43
+ branch="$(current_branch "$path")"
44
+ dirty=""
45
+ if is_dirty "$path"; then dirty=" *"; fi
46
+ if [[ "$branch" == "$base" ]]; then
47
+ printf '%-22s %s%s\n' "$name" "$branch" "$dirty"
48
+ else
49
+ printf '%-22s %s%s (base: %s)\n' "$name" "$branch" "$dirty" "$base"
50
+ fi
51
+ done < <(workspace_repos)
52
+
53
+ while IFS= read -r name; do
54
+ printf '%-22s %s\n' "$name" "(unmapped — add it to repos: in .sassywares/config.yaml)"
55
+ done < <(unmapped_repos)
56
+
57
+ (( found > 0 )) || die "no git repos found under repos: — is the config complete?"
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env bash
2
+ # Check out each repo's base branch from `repos:`.
3
+ #
4
+ # Only switches a repo that is on some other branch. A dirty tree that git would
5
+ # clobber stops that repo with an error; nothing is stashed, reset, or forced.
6
+ #
7
+ # Usage: .sassywares/workspace/checkout-bases [--version]
8
+ set -euo pipefail
9
+
10
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ # shellcheck source=./_lib.sh
12
+ source "$SCRIPT_DIR/_lib.sh"
13
+
14
+ usage() {
15
+ cat <<EOF
16
+ Usage: .sassywares/workspace/checkout-bases [--version]
17
+
18
+ For each repo in the config's \`repos:\` map, check out its base branch:
19
+
20
+ - base exists locally → git checkout <base>
21
+ - base missing locally → fetch, then track $REMOTE/<base>
22
+ - already on the base → reported, untouched
23
+
24
+ Uncommitted changes are never discarded. Does not fetch or pull a repo it
25
+ already has on the base — run .sassywares/workspace/pull for that.
26
+ EOF
27
+ }
28
+
29
+ case "${1:-}" in
30
+ -h|--help) usage; exit 0 ;;
31
+ --version) printf '%s\n' "$WORKSPACE_GIT_VERSION"; exit 0 ;;
32
+ "") ;;
33
+ *) die "unknown argument: $1 (try --help)" ;;
34
+ esac
35
+
36
+ need_git
37
+
38
+ checkout_one() {
39
+ local name="$1" base="$2" path="$3"
40
+ log "$name → $base"
41
+
42
+ if [[ "$(current_branch "$path")" == "$base" ]]; then
43
+ ok "already on $base ($(short_head "$path"))"
44
+ return 0
45
+ fi
46
+
47
+ if git -C "$path" show-ref --verify --quiet "refs/heads/$base"; then
48
+ if ! out="$(git -C "$path" checkout "$base" 2>&1)"; then
49
+ warn "$name: could not check out $base — $(git_err "$out")"
50
+ return 1
51
+ fi
52
+ else
53
+ if ! out="$(git -C "$path" fetch --prune "$REMOTE" 2>&1)"; then
54
+ warn "$name: fetch failed — $(git_err "$out")"
55
+ return 1
56
+ fi
57
+ if ! git -C "$path" show-ref --verify --quiet "refs/remotes/$REMOTE/$base"; then
58
+ warn "$name: $REMOTE/$base does not exist"
59
+ return 1
60
+ fi
61
+ if ! out="$(git -C "$path" checkout -B "$base" "$REMOTE/$base" 2>&1)"; then
62
+ warn "$name: could not create $base — $(git_err "$out")"
63
+ return 1
64
+ fi
65
+ git -C "$path" branch --set-upstream-to="$REMOTE/$base" "$base" >/dev/null 2>&1 || true
66
+ fi
67
+
68
+ ok "on $base ($(short_head "$path"))"
69
+ }
70
+
71
+ found=0
72
+ failed=0
73
+
74
+ while IFS=$'\t' read -r name base; do
75
+ [[ -n "$name" ]] || continue
76
+ path="$ROOT/$name"
77
+ if ! repo_has_git "$path"; then
78
+ warn "$name: missing .git — skip"
79
+ continue
80
+ fi
81
+ found=1
82
+ checkout_one "$name" "$base" "$path" || failed=1
83
+ done < <(workspace_repos)
84
+
85
+ (( found > 0 )) || die "no git repos found under repos: — is the config complete?"
86
+
87
+ if (( failed != 0 )); then
88
+ die "one or more repos could not be put on their base"
89
+ fi
90
+
91
+ ok "checkout-bases done"
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env bash
2
+ # Fetch every repo in `repos:`, then fast-forward the checked-out branch.
3
+ #
4
+ # Nothing is discarded: dirty trees are kept, a branch without an upstream is
5
+ # fetched only, and a divergent branch is reported instead of merged. Switching
6
+ # branches is checkout-bases' job, not this one.
7
+ #
8
+ # Usage: .sassywares/workspace/pull [--version]
9
+ set -euo pipefail
10
+
11
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
+ # shellcheck source=./_lib.sh
13
+ source "$SCRIPT_DIR/_lib.sh"
14
+
15
+ usage() {
16
+ cat <<EOF
17
+ Usage: .sassywares/workspace/pull [--version]
18
+
19
+ For each repo in the config's \`repos:\` map:
20
+ 1. git fetch --prune $REMOTE
21
+ 2. git pull --ff-only when the checked-out branch has an upstream
22
+
23
+ Does not switch branches. Uncommitted changes are left as-is. A repo whose base
24
+ is missing from the remote is reported, never fixed here.
25
+ EOF
26
+ }
27
+
28
+ case "${1:-}" in
29
+ -h|--help) usage; exit 0 ;;
30
+ --version) printf '%s\n' "$WORKSPACE_GIT_VERSION"; exit 0 ;;
31
+ "") ;;
32
+ *) die "unknown argument: $1 (try --help)" ;;
33
+ esac
34
+
35
+ need_git
36
+
37
+ found=0
38
+ failed=0
39
+
40
+ while IFS=$'\t' read -r name base; do
41
+ [[ -n "$name" ]] || continue
42
+ path="$ROOT/$name"
43
+ if ! repo_has_git "$path"; then
44
+ warn "$name: missing .git — skip"
45
+ continue
46
+ fi
47
+ found=1
48
+ branch="$(current_branch "$path")"
49
+ log "$name ($branch)"
50
+ if is_dirty "$path"; then
51
+ warn "dirty working tree on $branch — pulling anyway (uncommitted changes kept)"
52
+ fi
53
+
54
+ if ! out="$(git -C "$path" fetch --prune "$REMOTE" 2>&1)"; then
55
+ warn "$name: fetch failed — $(git_err "$out")"
56
+ failed=1
57
+ continue
58
+ fi
59
+
60
+ if git -C "$path" show-ref --verify --quiet "refs/remotes/$REMOTE/$base"; then
61
+ ok "fetched $REMOTE/$base"
62
+ else
63
+ warn "$REMOTE/$base missing"
64
+ fi
65
+
66
+ upstream="$(git -C "$path" rev-parse --abbrev-ref '@{upstream}' 2>/dev/null || true)"
67
+ if [[ -z "$upstream" ]]; then
68
+ warn "no upstream for $branch — fetched only"
69
+ if [[ "$branch" == "$base" ]]; then
70
+ git -C "$path" branch --set-upstream-to="$REMOTE/$base" "$base" >/dev/null 2>&1 || true
71
+ upstream="$REMOTE/$base"
72
+ else
73
+ continue
74
+ fi
75
+ fi
76
+
77
+ if out="$(git -C "$path" pull --ff-only 2>&1)"; then
78
+ ok "pulled $branch ← $upstream"
79
+ else
80
+ warn "ff-only pull failed on $branch — $(git_err "$out")"
81
+ warn " .sassywares/workspace/checkout-bases puts it back on $base"
82
+ failed=1
83
+ fi
84
+ done < <(workspace_repos)
85
+
86
+ (( found > 0 )) || die "no git repos found under repos: — is the config complete?"
87
+
88
+ if (( failed != 0 )); then
89
+ die "one or more repos could not be fast-forwarded"
90
+ fi
91
+
92
+ ok "pull done"
@@ -1,30 +1,63 @@
1
1
  # Workspace install
2
2
 
3
- Read this only when the topology is `workspace`. Everything the snapshot owns
3
+ Read this only when the topology is `workspace`. Everything setup installs
4
4
  lives under one directory:
5
5
 
6
6
  ```text
7
7
  .sassywares/
8
8
  ├── config.yaml
9
9
  └── workspace/
10
- ├── snapshot the script (tracked)
11
- ├── .gitignore exclude patterns for the sync (tracked)
12
- └── mirror/ the side clone (untracked, self-excluded)
10
+ ├── snapshot sync + commit + push the glue (tracked)
11
+ ├── branches current branch per repo (tracked)
12
+ ├── pull fetch + fast-forward per repo (tracked)
13
+ ├── checkout-bases put each repo on its base (tracked)
14
+ ├── _lib.sh shared helpers; reads `repos:` (tracked)
15
+ ├── .gitignore exclude patterns for the sync (tracked)
16
+ └── mirror/ the side clone (untracked, self-excluded)
13
17
  ```
14
18
 
15
- - Copy `assets/snapshot` to `.sassywares/workspace/snapshot` and make it
16
- executable. **If a `snapshot` file is already there, say so and ask** — it may
17
- carry hand edits. Report the version you installed (`snapshot --version`) so a
18
- later `--check` can catch a stale copy.
19
- - Write `.sassywares/workspace/.gitignore` for excluded paths (vendored trees,
20
- generated dirs, `.run/`). One pattern per line, `.gitignore` style. Skip it when
21
- there is nothing to exclude; the sync works without it. The mirror needs no
22
- line — the script excludes it itself.
19
+ ## Install
20
+
21
+ - **Snapshot.** Copy `assets/snapshot` to `.sassywares/workspace/snapshot` and
22
+ make it executable. **If a `snapshot` file is already there, say so and ask** —
23
+ it may carry hand edits. Report the version you installed
24
+ (`snapshot --version`) so a later `--check` can catch a stale copy.
25
+ - **Git helpers.** Copy every file in `assets/workspace/` flat into
26
+ `.sassywares/workspace/` and make the three scripts executable. Report
27
+ `_lib.sh`'s `workspace-git-version` (or `branches --version`) the same way.
28
+ **Ask before overwriting any of them when they already exist** — from the first
29
+ install on they are the project's files, and extending them is expected.
30
+ - **Excludes.** Write `.sassywares/workspace/.gitignore` for excluded paths
31
+ (vendored trees, generated dirs, `.run/`). One pattern per line, `.gitignore`
32
+ style. Skip it when there is nothing to exclude; the sync works without it. The
33
+ mirror needs no line — the script excludes it itself.
23
34
  - Do **not** add a `.gitignore` at the project root, and do not modify one that
24
- exists.
35
+ exists. A workspace's only ignore file is the one in `workspace/`.
25
36
  - Do **not** run the snapshot during setup. It clones and pushes; that is the
26
37
  user's call. Offer `--dry-run` and run it only if they ask.
27
38
 
28
- The mirror remote comes from `git.remote`; the script falls back to the mirror's
39
+ ## What the git helpers are for
40
+
41
+ `branches`, `pull`, and `checkout-bases` do across the workspace's repos what the
42
+ same commands do in one repo, and they are a **human/CI convenience**. They read
43
+ the repo inventory and the PR bases from `repos:` in `config.yaml` — no second
44
+ base map, no probing, no inference: a repo missing from the map is reported as
45
+ unmapped, never guessed at.
46
+
47
+ - `branches` — current branch and dirty marker per repo, `(base: …)` when off base.
48
+ - `pull` — `fetch --prune`, then `pull --ff-only` when there is an upstream.
49
+ Dirty trees are kept, divergent branches are reported, nothing is reset.
50
+ - `checkout-bases` — switch to each repo's base, creating it from the remote when
51
+ it is missing locally. Refuses to clobber uncommitted work.
52
+
53
+ They are **not** part of the skill pipeline: skills resolve bases from the config
54
+ themselves and run their own git (`sassy-commit` cuts the branch, `sassy-babysit`
55
+ checks out the PR head). Never point a hook at these scripts. If a project wants
56
+ more — a status summary, a rebase flow — it extends its own copy; setup never
57
+ rewrites them without asking.
58
+
59
+ ## The mirror
60
+
61
+ The mirror remote comes from `git.remote`. The script falls back to the mirror's
29
62
  own `origin` once it exists. Never create the GitHub repository without the
30
63
  explicit approval described in the skill.
@@ -53,6 +53,7 @@ START="$(cd "$START" && pwd -P)"
53
53
 
54
54
  SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
55
55
  ASSET_SNAPSHOT="$SKILL_DIR/assets/snapshot"
56
+ ASSET_WORKSPACE="$SKILL_DIR/assets/workspace"
56
57
 
57
58
  # The nearest config wins, walking up. Report where it is if one exists.
58
59
  CONFIG=""
@@ -406,6 +407,29 @@ if (( CHECK )); then
406
407
  else
407
408
  bad "missing .sassywares/workspace/snapshot"
408
409
  fi
410
+
411
+ # The git helpers are optional as a set, but a half-installed or stale set is
412
+ # drift: they read `repos:` from this config, so a version skew means the
413
+ # project is running a reader written for a different map.
414
+ installed_lib="$ROOT/.sassywares/workspace/_lib.sh"
415
+ if [[ -f "$installed_lib" ]]; then
416
+ lib_v="$(sed -n 's/^# workspace-git-version: //p' "$installed_lib" | head -n 1)"
417
+ asset_lib_v="$(sed -n 's/^# workspace-git-version: //p' "$ASSET_WORKSPACE/_lib.sh" | head -n 1)"
418
+ if [[ -z "$lib_v" ]]; then
419
+ bad "installed .sassywares/workspace/_lib.sh has no workspace-git-version line"
420
+ elif [[ -n "$asset_lib_v" && "$lib_v" != "$asset_lib_v" ]]; then
421
+ bad "installed workspace git helpers are v${lib_v}, this skill ships v${asset_lib_v} — re-run setup"
422
+ else
423
+ note " workspace-git v${lib_v}"
424
+ fi
425
+ for helper in branches pull checkout-bases; do
426
+ if [[ ! -x "$ROOT/.sassywares/workspace/$helper" ]]; then
427
+ bad "missing or not executable: .sassywares/workspace/$helper"
428
+ fi
429
+ done
430
+ else
431
+ note " no workspace git helpers installed (optional)"
432
+ fi
409
433
  while IFS= read -r name; do
410
434
  [[ -n "$name" ]] || continue
411
435
  base="$(detect_base "$ROOT/$name")"