@sassywares/skills 0.5.4 → 0.5.6

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/CONCEPTS.md CHANGED
@@ -56,4 +56,4 @@ A settled in-context brief produced by `sassy-plan` when the user picks "start w
56
56
 
57
57
  ## Sync
58
58
 
59
- There is no background watcher. Any agent that changes a skill must run `scripts/sync.sh` immediately, without asking for confirmation. The sync script commits the change, rebases on `origin/main`, and pushes. It installs nothing: a changed skill reaches consumers only when a new version is released and a consumer bumps its pin.
59
+ There is no background watcher. Any agent that changes a skill must run `scripts/sync.sh` immediately, without asking for confirmation. The sync script bumps the patch version, commits, pushes `main`, then tags and pushes the tag, which publishes to npm. It installs nothing: `npx skills add` reads this repo directly, and the npm package follows the tag the script creates.
package/README.md CHANGED
@@ -259,6 +259,14 @@ The fix gate offers four options:
259
259
 
260
260
  The artifact decides QA: a plan document carries manual QA tables that gate phase completion, an in-context brief does not. Gates are deliberate stops; the only way past one is the user's approval. `mode:auto` is accepted only by `sassy-ship` and `sassy-compound`.
261
261
 
262
+ ## Branching
263
+
264
+ `sassy-work`, `sassy-lfg`, and `sassy-debug` decide the branch **before they touch code**, from `commit.branch` (default `new`) compared against `base_map`. On `new`, the run cuts a branch and every commit — phases, docs, ship — lands on it, so no change ever exists on a base branch. A run that is already on a branch that is not a base reuses it, which is how a handed-off phase, a resumed run, or `sassy-lfg` after `sassy-work` converges instead of cutting a second branch.
265
+
266
+ A base is **any `base_map` value**, not just the remote default: in a repo whose `"*"` targets `staging`, standing on `staging` counts as standing on a base. That distinction is the whole point of the key — see [`skills/sassy-setup/references/branching.md`](skills/sassy-setup/references/branching.md).
267
+
268
+ The three skills take `branch:infer|current|new` to override the config for one run: `infer` (default) means the config decides, `new` forces a branch, `current` commits where you stand. `sassy-ship` never creates a branch; finding the head on a base branch with unpublished commits is a stop-and-ask.
269
+
262
270
  ## Repository layout
263
271
 
264
272
  ```text
@@ -313,12 +321,14 @@ pi install npm:@sassywares/skills
313
321
 
314
322
  ## Repository maintenance
315
323
 
316
- `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 — consumers get a change when they run `npx skills update` after a push. `AGENTS.md` documents the workflow for agents working on this repo.
324
+ `scripts/sync.sh` bumps the patch version, commits, pushes `main`, then tags and pushes the tag — so a change to a skill is released in one step. Any agent that changes a skill must run it immediately, without asking for confirmation. It installs nothing anywhere; consumers get a change when they run `npx skills update` after a push. `AGENTS.md` documents the workflow for agents working on this repo.
317
325
 
318
326
  ### Releases
319
327
 
320
328
  Publishing is tag-driven. The `Publish` workflow (`.github/workflows/publish.yml`) validates every `skills/*/SKILL.md`, checks that the tag matches `package.json`, and publishes `@sassywares/skills` to npm on a `v*` tag push.
321
329
 
330
+ `scripts/sync.sh` does this for you on `main`. The manual sequence is the fallback for recovering a failed run:
331
+
322
332
  ```bash
323
333
  npm version patch --no-git-tag-version
324
334
  git add -A && git commit -m "chore: release v$(node -p "require('./package.json').version")"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sassywares/skills",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "Lean, explicit-invocation Agent Skills for shipping fast: plan, work, review, validate, ship.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,9 +28,11 @@ Treat the reads as a snapshot; re-read branch and staged set right before commit
28
28
 
29
29
  **Never bypass hooks.** No `--no-verify`, and never pass it through to a project script this run invokes. A repo's git hooks are the layer that cannot be skipped, so disabling them silently removes an enforcement the project relies on.
30
30
 
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.
31
+ **Config.** Resolve `.sassywares/config.yaml` (nearest, walking up) per the `sassy-setup` skill's `references/config.md`. `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 `base_map` from the repo's own config when it has one, else `repos[<dir>]` from the workspace config, else the probed base **confirmed with the user** — named `<name>` when given, else from the change. `sassy-commit-push-pr` forwards its own `branch` choice here.
33
+ **Who may pass a token.** Precedence is explicit token > session instruction > config > skill default, and a token counts as explicit only when the **caller** supplied it — the user typed it, or an earlier gate of the same run authorized it. A skill must never pass its own default as a token: that promotes a default over the project's config and makes `commit.branch` unreachable. When the entry skill (`sassy-work`, `sassy-lfg`, `sassy-debug`) has already resolved the branch through `sassy-setup`'s `references/branching.md`, it passes `branch:current`, because the branch exists and the work belongs on it.
34
+
35
+ **Branch option.** `branch:current|new [<name>]`. `current` commits on the checked-out branch. `new` cuts a branch from `origin/<base>` — `<base>` is `base_map` from the repo's own config when it has one, else `repos[<dir>]` from the workspace config, else the probed base **confirmed with the user** — named `<name>` when given, else from the change. Use `new` only when no branch has been cut yet; the entry skills cut at the start of a run, so a `new` from them means the cut was skipped. `sassy-commit-push-pr` forwards its own `branch` choice here.
34
36
 
35
37
  **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
38
 
@@ -38,7 +40,7 @@ Treat the reads as a snapshot; re-read branch and staged set right before commit
38
40
 
39
41
  0. **Gather** the context above.
40
42
  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 <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.
43
+ 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 already exists, reuse it when it descends from the base; fork a suffix only when it has diverged, and never cut a second branch off the base for the same work. With `branch:current`, commit on the checked-out branch. With no token and no `commit.branch`, treat it as `new` and cut one. **Base means any `base_map` value, not just `origin/HEAD`**: `bash .agents/skills/sassy-setup/scripts/detect.sh --branch` answers it, and a branch it reports as `on-base` must be named in the summary you return — a warning nobody sees protects nothing.
42
44
  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
45
  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
46
  5. **Message** — imperative subject naming the outcome, not the file list. Body only when the motivation is not obvious.
@@ -16,7 +16,7 @@ disable-model-invocation: true
16
16
 
17
17
  **Options.** `branch:new|current` selects the branch policy below. `babysit:on|off` (default `on`) controls the closing babysit. `checkpoint` bounds that babysit to a single tick. `merge:on|off` and `delete-branch:on|off` (both default `off`) are forwarded unchanged to `sassy-babysit`.
18
18
 
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. `commit.branch` seeds the branch policy, `pr.*` seeds the babysit options, and `hooks.pr` supplies the `before_create` / `after_create` steps for this run's head.
19
+ **Config.** Resolve `.sassywares/config.yaml` (nearest, walking up) per the `sassy-setup` skill's `references/config.md`. `commit.branch` seeds the branch policy, `pr.*` seeds the babysit options, and `hooks.pr` supplies the `before_create` / `after_create` steps for this run's head. Precedence is explicit token > session instruction > config > skill default, and a token is explicit only when the **caller** supplied it; never pass this skill's own default as a token.
20
20
 
21
21
  **Keys are the head branch.** Resolve per head: the exact head, then the longest matching family (`release/*`), then `"*"`. Each point overrides independently. An entry is `<skill> <args…>` — split on the first whitespace and pass the args verbatim.
22
22
 
@@ -37,7 +37,7 @@ gh pr list --head <branch> --state open --json url,number
37
37
 
38
38
  Only an exit-0 empty list means "no open PR". A non-zero exit means **unknown** — resolve auth/connectivity first, never treat it as "none". Probe output is a snapshot; re-check branch/remote/PR state right before the push and the PR create.
39
39
 
40
- **Branch policy.** Default: if on the base branch or detached HEAD, create a feature branch named from the work and say so. `branch:current` or a plain request keeps the checked-out branch (that run pushes and skips the PR). Never ask whether to branch. Forward the resolved choice to `sassy-commit`: `branch:new <name>` when this run creates a branch, else `branch:current`.
40
+ **Branch policy.** The run's branch is normally already decided: `sassy-work` and `sassy-lfg` cut it at the start via `sassy-setup`'s `references/branching.md`, and `sassy-ship` forwards `branch:current`. So the default here is to commit on the checked-out branch when it is not a base. Only when it *is* a base or a detached HEAD — a bare invocation with no orchestrator above it — 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>` only when this run creates a branch, else `branch:current`.
41
41
 
42
42
  **Base branch.** `<base>` is `base_map[<current head>]` from the nearest config — a nested repo's own config wins over the workspace's `repos[<dir>]` — falling back to `base_map["*"]`, else the probed remote HEAD **confirmed with the user before use**. The map is the reason a release PR targets `main` while the work sits on `staging`, with no token passed. An explicit `base:<branch>` token wins over all of them — use it when this PR targets something other than the day-to-day base, such as a release PR aimed at `main` while work lands on `staging`. **Never use `defaultBranchRef` as the PR base when the config names one.** Pass it explicitly: `gh pr create --base <base>`.
43
43
 
@@ -5,7 +5,7 @@ description: >-
5
5
  through sassy-cycle (fix, review, validate), and finally ship it
6
6
  through sassy-ship after code approval. Never auto-commits. Use when asked to
7
7
  debug or fix a failure.
8
- argument-hint: "[issue reference, error message, test path, or description]"
8
+ argument-hint: "[issue reference, error message, test path, or description] [branch:infer|current|new]"
9
9
  disable-model-invocation: true
10
10
  ---
11
11
 
@@ -15,6 +15,8 @@ Find the root cause of a failure, get approval for the fix, fix it through the s
15
15
 
16
16
  **Done when:** the full causal chain is stated with `file:line` evidence, the fix gate was answered with one of its four options, and either the fix has passed the quality cycle and shipped after code approval, or the run ended at the fix gate.
17
17
 
18
+ **Config.** Resolve `.sassywares/config.yaml` (nearest, walking up) per the `sassy-setup` skill's `references/config.md`. `commit.branch` decides whether the fix needs its own branch; `sassy-work` makes that decision when the fix starts, because the branch belongs to the work and not to the diagnosis.
19
+
18
20
  **Escalate rather than persist:** 2–3 hypotheses exhausted without confirmation, or 3 failed fix attempts, means diagnose *why* instead of trying again. One hypothesis, one change at a time — never shotgun-debug. Before escalating, re-search the prior learnings with the keywords of what you just ruled out: an exhausted hypothesis list is often a documented trap.
19
21
 
20
22
  ## Phase 0 — Triage
@@ -57,7 +59,11 @@ This gate is mandatory even when the original request said "fix the bug". Do not
57
59
 
58
60
  ### Option 1 — Fix it now
59
61
 
60
- Load the `sassy-work` skill with the fix brief as the work brief and the diagnosis from Phase 1 as the intent reference. Work runs one cycle, presents its own code approval gate, commits, and stops before ship. Continue to Phase 3.
62
+ Load the `sassy-work` skill with the fix brief as the work brief and the diagnosis from Phase 1 as the intent reference, forwarding this run's `branch:` token if it was given. Work runs one cycle, presents its own code approval gate, commits, and stops before ship.
63
+
64
+ The branch decision belongs to work, not to this phase: diagnosis writes nothing, so cutting a branch before the fix gate would leave an empty branch every time the gate ends in *Write a plan*, *Hand off*, or *Stop here*. Work resolves it per `sassy-setup`'s `references/branching.md` before it touches code, so the fix is on its own branch from the first edit and still never lands on a base branch.
65
+
66
+ Continue to Phase 3.
61
67
 
62
68
  ### Option 2 — Write a plan
63
69
 
@@ -4,7 +4,7 @@ description: >-
4
4
  Execute a whole sassy plan phase by phase, or one settled in-context brief,
5
5
  then close out via sassy-ship after code approval. The orchestrator only
6
6
  coordinates. Best for small plans that one context can hold.
7
- argument-hint: "<plan path | brief>"
7
+ argument-hint: "<plan path | brief> [branch:infer|current|new]"
8
8
  disable-model-invocation: true
9
9
  ---
10
10
 
@@ -14,7 +14,7 @@ disable-model-invocation: true
14
14
 
15
15
  **The orchestrator does not do work.** Read the plan, dispatch `sassy-cycle` per phase, then `sassy-ship`. Never edit application code. Keep the main context small — all heavy work happens in subagents.
16
16
 
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.
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; `commit.branch` decides whether the run needs its own branch.
18
18
 
19
19
  ## Input
20
20
 
@@ -38,18 +38,17 @@ A brief has no QA table, so its gate is code approval alone. Never invent, enabl
38
38
  ## Steps
39
39
 
40
40
  1. **Load the plan.** Read the plan and its Phase tracking table. If phases are already `In progress` from an interrupted run, ask once whether to resume at the gate or rerun those cycles, then continue from the answer.
41
- 2. **Run every phase, in order.** For each phase not `Done`:
41
+ 2. **Resolve the branch, before touching code.** Follow the `sassy-setup` skill's `references/branching.md` with this run's `branch:` token, if any. LFG runs every phase in one session, so the branch is cut once here and every phase commits onto it. Report the branch and its start point in the gate.
42
+ 3. **Run every phase, in order.** For each phase not `Done`:
42
43
  - Set the phase `In progress`, then load the `sassy-cycle` skill and follow it, with work brief = the phase's goal/work/"Done when", and intent reference = the plan path and phase.
43
44
  - On `Status: done` and `Validator: Conforms`, leave the phase `In progress` — its QA rows are still owed, and only the gate closes them.
44
45
  - On `Status: blocked`, mark it `Blocked` and stop the run.
45
- 3. **The gate.** After the last phase, report: the plan path, each phase's status, what changed per phase, and the validator/reviewer summaries. Then give every phase's QA checklist verbatim — row ID, check, how to run it, what passing looks like — with unreported rows `Pending`. Ask for both decisions: code approval and the QA result per row. Wait. Do not commit.
46
- 4. **Resolve the gate.** Three outcomes:
46
+ 4. **The gate.** After the last phase, report: the plan path, each phase's status, what changed per phase, and the validator/reviewer summaries. Then give every phase's QA checklist verbatim — row ID, check, how to run it, what passing looks like — with unreported rows `Pending`. Ask for both decisions: code approval and the QA result per row. Wait. Do not commit.
47
+ 5. **Resolve the gate.** Three outcomes:
47
48
  - **Code approved and every QA row passes** → mark the rows `Done` with the user's notes, mark the phases `Done`, then run the close-out below.
48
49
  - **Code approved, QA still pending** → tick nothing, mark nothing `Done`, commit nothing, ship nothing. Report which rows are outstanding and how to resume (`/skill:sassy-lfg <plan>`), and stop.
49
50
  - **Any QA row fails** → run one more `sassy-cycle` on that phase with the failing rows as the intent reference, then present the gate again. A row that still fails is `Blocked` with the reason and stops the run.
50
- 5. **Close-out.** On both gates passing, load the `sassy-ship` skill and follow it. Do only the close-out steps the user approves. Babysit options (`merge:on|off`, `delete-branch:on|off`) are forwarded through `sassy-ship` to `sassy-babysit`.
51
-
52
- Work commits to the current branch. `sassy-ship` creates the shipping branch at close-out when the checked-out branch is the repo default.
51
+ 6. **Close-out.** On both gates passing, load the `sassy-ship` skill and follow it. Do only the close-out steps the user approves. Babysit options (`merge:on|off`, `delete-branch:on|off`) are forwarded through `sassy-ship` to `sassy-babysit`. The branch from step 2 already carries the work, so ship commits and opens the PR on it; ship never creates the branch.
53
52
 
54
53
  ## Brief constraints
55
54
 
@@ -63,15 +62,17 @@ Every worker brief this run dispatches must include:
63
62
 
64
63
  When the argument is not a plan path, the brief is the one phase:
65
64
 
66
- 1. Run one `sassy-cycle` with the brief as the work brief and its decisions / done-when as the intent reference. Do not split it into multiple phases.
67
- 2. Present the code approval gate with no QA checklist, and wait.
68
- 3. On code approval, run `sassy-ship`. Do not run `sassy-plan` or write a plan file.
65
+ 1. Resolve the branch exactly as the plan path does — `references/branching.md`, this run's `branch:` token — before the cycle runs.
66
+ 2. Run one `sassy-cycle` with the brief as the work brief and its decisions / done-when as the intent reference. Do not split it into multiple phases.
67
+ 3. Present the code approval gate with no QA checklist, and wait.
68
+ 4. On code approval, run `sassy-ship`. Do not run `sassy-plan` or write a plan file.
69
69
 
70
70
  ## Hard rules
71
71
 
72
72
  | Rule | Requirement |
73
73
  |------|-------------|
74
74
  | Orchestrator only | Delegate every phase to `sassy-cycle`. Never read source or edit code. |
75
+ | Branch before code | Resolve the branch per `references/branching.md` before any phase runs, so no change lands on a base branch. |
75
76
  | Phases in plan order | Phases run one after another, in plan order. |
76
77
  | Done needs both gates | Mark a phase `Done` only when the user approves the code and every QA row for it is `Done`. |
77
78
  | QA rows follow the user | Tick a QA row `Done` only from the user's explicit confirmation; keep unreported rows `Pending`; log a failed row as a finding for one more cycle. |
@@ -27,6 +27,7 @@ disable-model-invocation: true
27
27
  - **Artifacts** — offer the detected paths as the default; take overrides.
28
28
  - **Knowledge** — `knowledge.read` defaults to the solutions dir plus the concepts file. Ask whether there are extra folders to read (a hand-written `notes/learnings/`, a `docs/adr/`).
29
29
  - **Base map** — detection proposes `"*"` from the probed remote default. Ask where ordinary work branches from, and whether the default branch itself releases to another branch (a `staging: main` rung). Never write `main` as a guess. In `workspace` mode, show the `repos` map and get a yes on every row — a probed base is only the remote's default.
30
+ - **`commit.branch`** — default `new`: the run cuts a branch before the first change and never commits on a base branch. Offer `current` for a repo that genuinely wants work committed where it stands, and say plainly that `current` lets a run commit onto its base branch.
30
31
  - **`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
32
  - **Hooks** — offer, do not impose. Ask whether the project wants steps injected at `before_create`, `after_create`, `before_merge`, or `after_merge`, and under which head branch (`"*"` for all). Entries may carry args (`<skill> <args…>`); a hook skill's args are its own vocabulary. Leave them `[]` when there is nothing to add. **Never write a hook skill yourself** — hooks are project skills, and copying one of ours into that role is the fork this mechanism exists to prevent.
32
33
  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.
@@ -19,7 +19,7 @@ The vendored skills and the lock are committed; `.claude/skills` is a link and i
19
19
 
20
20
  `.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.
21
21
 
22
- If a skill tells you another skill is missing, run `skills:sync` and restart. Substituted package managers: run the same script through `npm`, `pnpm`, or `yarn`.
22
+ If a skill tells you another skill is missing, run `npx skills add sassywares/sassy-skills` (or `npx skills experimental_install` to restore exactly what the lock pins) and restart.
23
23
 
24
24
  ## Config
25
25
 
@@ -0,0 +1,97 @@
1
+ # Branching
2
+
3
+ One branch decision per run, made **before** the first change. `sassy-work`,
4
+ `sassy-lfg`, and `sassy-debug` all resolve it here, through one detector, so the
5
+ three cannot drift apart the way three hand-copied paragraphs would.
6
+
7
+ ## Resolve
8
+
9
+ ```bash
10
+ bash .agents/skills/sassy-setup/scripts/detect.sh --branch [--policy current|new]
11
+ ```
12
+
13
+ `--policy` is the `branch:` token: `infer` (the default), `current`, or `new`.
14
+ Omit it to let `commit.branch` decide; `branch:infer` means exactly that and is
15
+ passed only to be explicit.
16
+
17
+ Output:
18
+
19
+ | Key | Meaning |
20
+ |---|---|
21
+ | `topology` | `repo`, `workspace`, or `none` |
22
+ | `configured` | `commit.branch` from the config, or `none` |
23
+ | `policy` | what actually applies: the token, else the config, else `new` |
24
+ | `head` | the checked-out branch, or `none` when detached |
25
+ | `state` | `on-base`, `on-branch`, `detached`, or `no-git` |
26
+ | `action` | `create`, `reuse`, `commit-here`, or `refuse` |
27
+ | `start-point` | where a new branch is cut from — `origin/<head>`, never local `HEAD` |
28
+ | `unpublished` | commits on local `<head>` that are not on `origin/<head>` |
29
+ | `pr-base` | the base a PR from this run targets |
30
+ | `pr-base-source` | `config`, `probed`, or `unknown` — a probed base is confirmed with the user before use |
31
+
32
+ A base branch is **any `base_map` value**, plus the remote default. That is why
33
+ `staging` counts as a base in a repo whose `base_map` routes `"*"` to it: the
34
+ remote default alone is not the base, it is only one of them.
35
+
36
+ ## Token authorship
37
+
38
+ Pass `--policy` only when the **caller** asked for it — the user typed
39
+ `branch:new`, or an earlier gate of this same run authorized it.
40
+
41
+ Never pass a token that is merely this skill's own default. Doing that launders
42
+ a default into "explicit token", which outranks the project's config and makes
43
+ `commit.branch` unreachable. Omitting `--policy` is how a skill defers to
44
+ config. This applies to every token these skills forward, not just `branch`.
45
+
46
+ ## Act on `action`
47
+
48
+ ### `create`
49
+
50
+ 1. `unpublished` must be `0`. If not, **stop and ask**: cutting from local HEAD
51
+ would carry commits stranded on the base into the new branch, and cutting
52
+ from `origin/<head>` would abandon them. Name the count, the branch, and the
53
+ choice.
54
+ 2. Name it `<type>/<slug>` from the plan filename or the topic, deterministically,
55
+ so a retried run converges on the same name instead of forking `-2`.
56
+ 3. If that name already exists: reuse it when it descends from `start-point`;
57
+ stop and ask when it has diverged.
58
+ 4. `git fetch <remote> <head>`, then `git checkout -b <name> <start-point>`.
59
+ 5. Report the branch, the start point, and `pr-base`.
60
+
61
+ ### `reuse`
62
+
63
+ Report `head` and carry on. Do **not** re-cut because `commit.branch` is `new`:
64
+ a second phase, a resumed run, or a handed-off session is already standing on
65
+ the branch the first one cut. That is the whole reason this is inferred rather
66
+ than forced.
67
+
68
+ ### `commit-here`
69
+
70
+ Only reachable with `policy: current`, which is an explicit statement that
71
+ committing on the base branch is intended. Proceed, and say in the run's gate
72
+ summary that the commits are landing on `<head>` and that `pr-base` is
73
+ `<pr-base>`. Never infer this from a skill default.
74
+
75
+ ### `refuse`
76
+
77
+ Detached HEAD with `policy: current`, or `topology: none`. Report and stop;
78
+ there is no branch to commit to.
79
+
80
+ ## Workspace
81
+
82
+ Resolve once at the workspace root; the run uses **one branch name across every
83
+ nested repo**. The detector prints each repo's head and state, so cut inside the
84
+ repos whose state is `on-base` and reuse the rest. A nested repo carrying its own
85
+ `commit.branch` that disagrees is printed as `drift` — that is a config bug to
86
+ report, not a per-repo override, because one run cannot follow two policies.
87
+
88
+ ## After the branch exists
89
+
90
+ Downstream steps forward `branch:current` to `sassy-commit`, because the decision
91
+ has already been made and the commits belong on that branch. `sassy-ship` never
92
+ creates the branch — by the time it runs, the branch carries the work. If it ever
93
+ finds the run's head on a base branch with unpublished commits, that is a
94
+ mis-run: stop and ask, never relocate commits silently.
95
+
96
+ Report the branch and its start point in every gate summary. A decision the user
97
+ cannot see is a decision they cannot correct.
@@ -25,6 +25,15 @@ explicit token in the request > instruction in this session
25
25
  > .sassywares/config.yaml > skill default
26
26
  ```
27
27
 
28
+ **A token is explicit only when the caller supplied it** — the user typed it, or
29
+ an earlier gate of the same run authorized that side effect. A skill must never
30
+ pass its own default as a token, to another skill or to itself: once a default
31
+ sits in the token slot, nothing downstream can tell it apart from a user's
32
+ choice, so it silently outranks the project's config. A skill defers to config
33
+ by **omitting** the token, never by passing a value it invented. That is how
34
+ `commit.branch` became unreachable while every skill still read as if it
35
+ explained the rule correctly.
36
+
28
37
  Read it once per run. An orchestrator resolves every config-backed value and
29
38
  passes the resolved values into subagent briefs; subagents never re-read the
30
39
  file. A skill invoked directly resolves it itself.
@@ -76,7 +85,7 @@ hooks:
76
85
  | `git.remote` | mirror of `git.repo` | Workspace only: mirror remote (URL or `owner/name`) |
77
86
  | `git.mirror_branch` | `main` | Workspace only: the branch the mirror pushes to |
78
87
  | `repos` | `{}` | Workspace only: repo dir name → the base its PRs target |
79
- | `commit.branch` | `current` | `current` commits where you are; `new` cuts a branch |
88
+ | `commit.branch` | `new` | Resolved once per run, before the first change. `new` never commits on a base branch: `sassy-work`, `sassy-lfg`, and `sassy-debug` cut a branch first. `current` commits where you stand, base branch included |
80
89
  | `pr.babysit` | `true` | Whether close-out babysits the PR it opened |
81
90
  | `pr.merge` | `false` | `true` authorizes merging a single babysat PR |
82
91
  | `pr.delete_branch` | `false` | Delete the head branch once merged |
@@ -143,6 +152,35 @@ without asking, so the version line is how staleness surfaces.
143
152
  In `workspace` and `none`, changes still land on disk. Nothing is silently
144
153
  dropped; the refusal names why and what to run instead.
145
154
 
155
+ ## `commit.branch` — decide once, before code
156
+
157
+ Resolved once per run, by the entry skill (`sassy-work`, `sassy-lfg`,
158
+ `sassy-debug`), **before the first change**, through
159
+ `sassy-setup/references/branching.md`. Not at commit time: committing first and
160
+ re-branching later leaves the commits on a base branch, and cutting the new
161
+ branch from `origin/<base>` then leaves them behind entirely.
162
+
163
+ | Value | Meaning |
164
+ |-------|---------|
165
+ | `current` | Commit where you stand, even on a base branch. An explicit statement that this repo's work in progress is acceptable |
166
+ | `new` (default) | Never commit on a base branch. Cut one first, then stay on it for the rest of the run |
167
+
168
+ A branch that is already checked out and is **not** a base is reused, whatever
169
+ the config says. That is what makes a second phase, a resumed run, or a
170
+ handed-off session converge on the branch the first one cut instead of cutting
171
+ another.
172
+
173
+ A base is **any `base_map` value**, plus the remote default — not the remote
174
+ default alone. In the config above `staging` is a base because `"*"` targets it,
175
+ even though `origin/HEAD` is `main`. Comparing only against `origin/HEAD` is how
176
+ phase work once got committed straight onto `staging`.
177
+
178
+ The `branch:infer|current|new` token on those skills overrides this key for one
179
+ run; `infer` means this key decides. In a workspace the key is resolved from the
180
+ workspace config for the whole run, one branch name in every repo; a nested
181
+ repo whose own `commit.branch` disagrees is drift, because one run cannot follow
182
+ two policies.
183
+
146
184
  ## Resolution chains
147
185
 
148
186
  **Effective repo.** Inside a git work tree, the nearest toplevel is the repo —
@@ -186,9 +224,10 @@ config to. When a repo has both, the nested config wins and `--check` reports
186
224
  the disagreement rather than picking silently. A nested config needs no ladder
187
225
  of its own unless its PRs target more than one branch.
188
226
 
189
- **Branch name**, one per run. Derived once from the plan slug or topic before the
190
- first commit, then passed to every repo. Multi-repo runs pass
191
- `branch:new <name>`; a per-repo derived name would diverge.
227
+ **Branch name**, one per run. Derived once from the plan slug or topic when the
228
+ run starts, then used in every repo. The branch is cut at the start of the run,
229
+ not at commit time, so every repo sits on it before any change exists; each
230
+ repo cuts from its own upstream base and reuses the name in the others.
192
231
 
193
232
  Cut from the base, then verify:
194
233
 
@@ -7,12 +7,19 @@
7
7
  # Usage:
8
8
  # detect.sh [--root DIR] print a proposed config to stdout
9
9
  # detect.sh [--head BRANCH] resolve the base and hooks for one head
10
+ # detect.sh --branch [--policy P] resolve the branch decision for this run
10
11
  # detect.sh [--root DIR] --check compare the existing config to reality
12
+ #
13
+ # --policy takes infer|current|new: the skill-level token. infer (the default)
14
+ # defers to commit.branch in the config, and new is the fallback when the config
15
+ # is silent. --branch is read-only; it prints the decision, it never cuts.
11
16
  set -euo pipefail
12
17
 
13
18
  ROOT_ARG=""
14
19
  HEAD_ARG=""
20
+ POLICY_ARG=""
15
21
  CHECK=0
22
+ BRANCH=0
16
23
 
17
24
  while (( $# )); do
18
25
  case "$1" in
@@ -20,8 +27,11 @@ while (( $# )); do
20
27
  --root=*) ROOT_ARG="${1#*=}" ;;
21
28
  --head) shift; [[ $# -ge 1 ]] || { echo "missing BRANCH after --head" >&2; exit 2; }; HEAD_ARG="$1" ;;
22
29
  --head=*) HEAD_ARG="${1#*=}" ;;
30
+ --policy) shift; [[ $# -ge 1 ]] || { echo "missing POLICY after --policy" >&2; exit 2; }; POLICY_ARG="$1" ;;
31
+ --policy=*) POLICY_ARG="${1#*=}" ;;
32
+ --branch) BRANCH=1 ;;
23
33
  --check) CHECK=1 ;;
24
- -h|--help) sed -n '2,10p' "${BASH_SOURCE[0]}" | sed 's|^# \{0,1\}||'; exit 0 ;;
34
+ -h|--help) sed -n '2,14p' "${BASH_SOURCE[0]}" | sed 's|^# \{0,1\}||'; exit 0 ;;
25
35
  *) echo "unknown argument: $1" >&2; exit 2 ;;
26
36
  esac
27
37
  shift
@@ -414,6 +424,136 @@ if (( CHECK )); then
414
424
  exit 0
415
425
  fi
416
426
 
427
+ # --- Branch decision -------------------------------------------------------
428
+ # Read-only. Resolves commit.branch into the single decision the entry skills
429
+ # (sassy-work, sassy-lfg, sassy-debug) make before they start: cut a branch,
430
+ # reuse the one checked out, or commit where you stand.
431
+ #
432
+ # Every base_map VALUE counts as a base branch, not just the remote default.
433
+ # tap2's day-to-day base is `staging`, so comparing only against origin/HEAD
434
+ # declared staging "not a base" and let phase work commit onto it.
435
+ if (( BRANCH )); then
436
+ policy="${POLICY_ARG:-infer}"
437
+ case "$policy" in
438
+ infer|current|new) ;;
439
+ *) echo "unknown --policy: $policy (expected infer, current, or new)" >&2; exit 2 ;;
440
+ esac
441
+ configured="$(yflat "commit${US}branch" 2>/dev/null || true)"
442
+ [[ "$policy" == "infer" ]] && policy="${configured:-new}"
443
+
444
+ # base_heads prints every branch this config treats as a base: each base_map
445
+ # value, plus the remote default it inherits from. With no config to speak,
446
+ # the probed remote HEAD is the only base there is.
447
+ base_heads() {
448
+ local v out=""
449
+ out="$(yaml_flat "$CONFIG" 2>/dev/null | awk -F'\t' -v us="$US" \
450
+ '{n=split($1,a,us); if (n==3 && a[1]=="git" && a[2]=="base_map") print $2}' | sort -u || true)"
451
+ if [[ "$TOPOLOGY" == "repo" ]]; then
452
+ v="$(git -C "$ROOT" symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||' || true)"
453
+ [[ -n "$v" ]] && out="$out"$'\n'"$v"
454
+ fi
455
+ out="$(printf '%s\n' "$out" | grep -v '^$' | sort -u || true)"
456
+ # Probing through gh costs a network call, so only when nothing else said.
457
+ [[ -n "$out" ]] || out="$(detect_base "$ROOT" 2>/dev/null || true)"
458
+ printf '%s\n' "$out" | grep -v '^$' || true
459
+ }
460
+
461
+ # in_base_heads BRANCH — membership without a pipe: `base_heads | grep -q`
462
+ # trips pipefail, because grep exits on the first match and SIGPIPEs the
463
+ # producer, so a branch that IS a base reported as if it were not.
464
+ in_base_heads() {
465
+ local want="$1" b
466
+ while IFS= read -r b; do
467
+ [[ "$b" == "$want" ]] && return 0
468
+ done < <(base_heads)
469
+ return 1
470
+ }
471
+
472
+ printf 'topology: %s\n' "$TOPOLOGY"
473
+ printf 'config: %s\n' "${CONFIG:-none}"
474
+ printf 'configured: %s\n' "${configured:-none}"
475
+ printf 'policy: %s\n' "$policy"
476
+
477
+ if [[ "$TOPOLOGY" == "none" ]]; then
478
+ printf 'state: no-git\naction: refuse\n'
479
+ exit 0
480
+ fi
481
+
482
+ if [[ "$TOPOLOGY" == "workspace" ]]; then
483
+ # The run shares one branch name across every repo, so the decision is made
484
+ # once, here, and applied inside each nested repo. A nested repo carrying
485
+ # its own commit.branch that disagrees is drift, not an override: it would
486
+ # give one run two branch policies.
487
+ while IFS= read -r dir; do
488
+ [[ -n "$dir" ]] || continue
489
+ head="$(git -C "$ROOT/$dir" branch --show-current 2>/dev/null || true)"
490
+ if [[ -z "$head" ]]; then
491
+ printf 'repo %s: head=detached state=detached\n' "$dir"
492
+ elif in_base_heads "$head"; then
493
+ printf 'repo %s: head=%s state=on-base\n' "$dir" "$head"
494
+ else
495
+ printf 'repo %s: head=%s state=on-branch\n' "$dir" "$head"
496
+ fi
497
+ own=""
498
+ if [[ -f "$ROOT/$dir/.sassywares/config.yaml" ]]; then
499
+ own="$(yflat_in "$ROOT/$dir/.sassywares/config.yaml" "commit${US}branch" 2>/dev/null || true)"
500
+ fi
501
+ [[ -n "$own" && "$own" != "$policy" ]] && printf 'drift %s: commit.branch=%s\n' "$dir" "$own"
502
+ done < <(nested_repos)
503
+ exit 0
504
+ fi
505
+
506
+ head="${HEAD_ARG:-$(git -C "$ROOT" branch --show-current 2>/dev/null || true)}"
507
+ printf 'head: %s\n' "${head:-none}"
508
+
509
+ if [[ -z "$head" ]]; then
510
+ state=detached
511
+ elif in_base_heads "$head"; then
512
+ state=on-base
513
+ else
514
+ state=on-branch
515
+ fi
516
+ printf 'state: %s\n' "$state"
517
+
518
+ case "$state:$policy" in
519
+ on-branch:*) action=reuse ;;
520
+ on-base:new) action=create ;;
521
+ on-base:current) action=commit-here ;;
522
+ detached:new) action=create ;;
523
+ *) action=refuse ;;
524
+ esac
525
+ printf 'action: %s\n' "$action"
526
+
527
+ # A new branch starts at the upstream tip, never at local HEAD: commits that
528
+ # were stranded on the base earlier must not ride along silently.
529
+ if [[ -n "$head" ]] && git -C "$ROOT" rev-parse --verify --quiet "refs/remotes/origin/$head" >/dev/null 2>&1; then
530
+ printf 'start-point: origin/%s\n' "$head"
531
+ printf 'unpublished: %s\n' "$(git -C "$ROOT" rev-list --count "origin/$head..$head" 2>/dev/null || echo 0)"
532
+ else
533
+ printf 'start-point: %s\n' "${head:-none}"
534
+ printf 'unpublished: unknown\n'
535
+ fi
536
+
537
+ if [[ "$action" == "create" ]]; then
538
+ # The head will be a brand-new name, so only the "*" rung can apply to it.
539
+ pr_base="$(yflat "git${US}base_map${US}*" 2>/dev/null || true)"
540
+ pr_src=config
541
+ else
542
+ pr_base="$(base_for_head "$head" 2>/dev/null || true)"
543
+ pr_src=config
544
+ fi
545
+ if [[ -z "$pr_base" ]]; then
546
+ # No config named a base. A probed value is not a decision: the skills must
547
+ # confirm it with the user before it becomes a PR target.
548
+ pr_base="$(detect_base "$ROOT" 2>/dev/null || true)"
549
+ pr_src=probed
550
+ fi
551
+ [[ -n "$pr_base" ]] || pr_src=unknown
552
+ printf 'pr-base: %s\n' "$pr_base"
553
+ printf 'pr-base-source: %s\n' "$pr_src"
554
+ exit 0
555
+ fi
556
+
417
557
  # --- Emit a proposal -------------------------------------------------------
418
558
  # Every key carries its meaning and its allowed values. The file is the
419
559
  # documentation a teammate reads, so the comments are part of the output, not
@@ -532,12 +672,12 @@ esac
532
672
  cat <<'EOF'
533
673
 
534
674
  # --- Commit ---------------------------------------------------------------
535
- # current: commit on the checked-out branch.
536
- # new: cut a branch from the base map for the current head; name it in multi-repo runs so
537
- # every repo gets the same branch name.
675
+ # current: commit on the checked-out branch, even on a base branch.
676
+ # new: never commit on a base branch — sassy-work, sassy-lfg, and sassy-debug
677
+ # cut a branch at the start of the run and stay on it.
538
678
  commit:
539
679
  EOF
540
- kv branch current 34 '# current | new'
680
+ kv branch new 34 '# current | new'
541
681
  cat <<'EOF'
542
682
 
543
683
  # --- Pull requests --------------------------------------------------------
@@ -26,8 +26,10 @@ disable-model-invocation: true
26
26
  2. **Compound (conditional).** If the work produced a qualifying learning, dispatch a `worker` whose task is: *load the `sassy-compound` skill and follow it with `mode:auto`* — with a compact learnings brief (the solved problems, root causes, and solutions taken from the cycle results, not from re-reading the tree). It always gets `mode:auto` because ship's own gate already covered the approval. Skip cleanly when nothing qualifies, and skip entirely when `knowledge.write` is `false`.
27
27
  3. **Handbook (conditional).** If the feature is self-contained and independent enough that a teammate would benefit from a handover, dispatch a `worker` whose task is: *load the `sassy-handbook` skill and follow it* — with the plan path and topic. The worker reads the tree itself. Skip for small or tightly-coupled changes.
28
28
  4. **Commit or snapshot.** After the review gate — never before it — the docs and any remaining code become durable.
29
- - **`git.repo` is a remote.** Dispatch a `worker`: *load the `sassy-commit` skill and follow it*, naming that the commit must include the plan, any new solutions docs, the concepts file, `AGENTS.md`, and any handbook just written — resolved from `artifacts.*`, never hardcoded. Use `branch:current` when already on a feature branch, so the docs land with the phase commits. When the checked-out branch is the base branch, use `branch:new <name>` so the branch is created from `origin/<base>` and carries every phase commit; a workspace run passes the run's shared branch name. Never push directly to the base branch. It returns the hash and subject.
30
- - **`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`.
29
+ - **`git.repo` is a remote.** Dispatch a `worker`: *load the `sassy-commit` skill and follow it with `branch:current`*, 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. The branch already exists: `sassy-work` or `sassy-lfg` cut it before the first change, so the docs belong on it with the phase commits. It returns the hash and subject.
30
+
31
+ **Ship never creates the branch.** If this run's head is a base branch with unpublished commits, that is a mis-run — `sassy-work` was supposed to cut before touching code. Stop and ask, naming the branch and the count; never relocate commits silently, never push to the base.
32
+ - **`git.repo: workspace`.** Commit code inside each affected repo on the run's shared branch — cut once at the start of the run, `branch:current` from then on — then run `.sassywares/workspace/snapshot`. The snapshot is the workspace's commit and it runs every time, right after approval. A missing or outdated `.sassywares/workspace/snapshot` is a blocker: report it and say to run `sassy-setup`.
31
33
  - **`git.repo: none`.** No commit exists here. Report that the work is not persisted and stop.
32
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 own `base_map` when it carries a `.sassywares/config.yaml`, else its `repos` entry — and return a table. A touched repo missing from `repos`, or a base that cannot be resolved, is a stop-and-ask, never a guess. `none` opens nothing.
33
35
 
@@ -46,7 +48,8 @@ disable-model-invocation: true
46
48
  | Delegate everything | The orchestrator runs no git, gh, or file work itself. |
47
49
  | Conditional docs | Compound and handbook are skipped when they do not qualify. |
48
50
  | Docs in the commit | New docs and pointer lines ship in the same change. |
49
- | One branch | Docs land on the current feature branch; from the base branch, ship creates a feature branch so every phase commit moves with it. |
51
+ | One branch | The branch is cut once, before the first change, by `sassy-work` or `sassy-lfg`. Ship and its docs commit land on it. |
52
+ | Never create the branch | Ship never cuts a branch. Finding the run's head on a base branch with unpublished commits is a stop-and-ask. |
50
53
  | Never default | Never push directly to the repo base branch. |
51
54
  | Snapshot after approval | In a workspace, the snapshot runs every time, immediately after the commit approval — never before it, never skipped. |
52
55
  | One PR per run | `pr.babysit` and `pr.merge` are single-PR. A fan-out gets one babysit run per PR and no automatic merge. |
@@ -37,7 +37,7 @@ Ask which one, unless the request already made it obvious. The answer changes th
37
37
 
38
38
  | Location | Path | Who gets it | Extra work |
39
39
  |---|---|---|---|
40
- | **Internal** — part of this suite | `skills/<name>/` in the sassy-skills repo | Everyone, once pushed to `main` | Name it `sassy-<name>`, run `scripts/sync.sh`; the npm package follows the next tag |
40
+ | **Internal** — part of this suite | `skills/<name>/` in the sassy-skills repo | Everyone, once pushed to `main` | Name it `sassy-<name>`, run `scripts/sync.sh` — it bumps the patch version, tags, and publishes |
41
41
  | **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 |
42
42
  | **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 |
43
43
 
@@ -5,7 +5,7 @@ description: >-
5
5
  hand off to a fresh session for the next phase (or close out via sassy-ship if
6
6
  it was the last). Also runs one settled in-context brief and stops before ship.
7
7
  The orchestrator only coordinates.
8
- argument-hint: "<plan path | brief> [phase:<n>]"
8
+ argument-hint: "<plan path | brief> [phase:<n>] [branch:infer|current|new]"
9
9
  disable-model-invocation: true
10
10
  ---
11
11
 
@@ -17,7 +17,7 @@ disable-model-invocation: true
17
17
 
18
18
  **The orchestrator does not do work.** Read the plan, dispatch `sassy-cycle`, apply its result, update the plan's tracking table, dispatch `sassy-commit` or the snapshot, and report. Never edit application code.
19
19
 
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.
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, and `commit.branch` decides whether this run needs its own branch.
21
21
 
22
22
  ## Input
23
23
 
@@ -46,26 +46,25 @@ Always gate before committing. One worker fixes findings, one re-check, then pre
46
46
 
47
47
  1. **Load the phase.** Read the plan and only the selected phase's section. If it is already `Done`, stop and report.
48
48
 
49
- If the phase is `In progress`, an earlier run was interrupted before both gates passed. Ask one question — resume at the gate, or re-run the cycle from scratch — and take the answer. Resuming skips straight to step 4 and does not rebuild the phase.
50
- 2. **Run the cycle.** Set the phase `In progress` in the Phase tracking table, then load the `sassy-cycle` skill and follow it, with:
49
+ If the phase is `In progress`, an earlier run was interrupted before both gates passed. Ask one question — resume at the gate, or re-run the cycle from scratch — and take the answer. Resuming skips straight to step 5 and does not rebuild the phase.
50
+ 2. **Resolve the branch, before touching code.** Follow the `sassy-setup` skill's `references/branching.md` with this run's `branch:` token, if any. On `create`, cut the branch now — not at commit time — so no change ever exists on a base branch. On `reuse`, stay where you are, which is how a handed-off later phase or a resumed run lands on the branch the first phase cut. Carry the branch and its start point into every gate summary you present. Stop and ask when the reference says to.
51
+ 3. **Run the cycle.** Set the phase `In progress` in the Phase tracking table, then load the `sassy-cycle` skill and follow it, with:
51
52
  - work brief = the phase's goal, work items, and "Done when" criteria
52
53
  - intent reference = the plan path and phase
53
- 3. **Evaluate the phase.** On `Status: done` with `Validator: Conforms` and no critical review findings, note that the code is ready but leave the phase `In progress` — both gates must pass before it closes. On `Status: blocked`, set it `Blocked` in the Phase tracking table and stop; a blocked phase is a real outcome and needs no approval.
54
- 4. **Present the phase gate.** Report the phase, what changed, findings raised/resolved, the validator grade, and the reviewer summary. Then give the QA checklist verbatim: each row's ID, check, how to run it, and what passing looks like, with unreported rows `Pending`. Ask for both decisions — code approval and the QA result per row. Wait.
55
- 5. **Resolve the gate.** Three outcomes, nothing in between:
56
- - **Code approved and every QA row passes** → go to step 6.
54
+ 4. **Evaluate the phase.** On `Status: done` with `Validator: Conforms` and no critical review findings, note that the code is ready but leave the phase `In progress` — both gates must pass before it closes. On `Status: blocked`, set it `Blocked` in the Phase tracking table and stop; a blocked phase is a real outcome and needs no approval.
55
+ 5. **Present the phase gate.** Report the phase, what changed, findings raised/resolved, the validator grade, and the reviewer summary. Then give the QA checklist verbatim: each row's ID, check, how to run it, and what passing looks like, with unreported rows `Pending`. Ask for both decisions — code approval and the QA result per row. Wait.
56
+ 6. **Resolve the gate.** Three outcomes, nothing in between:
57
+ - **Code approved and every QA row passes** → go to step 7.
57
58
  - **Code approved, QA still pending** → record that code is approved but leave the phase `In progress`, tick nothing, commit nothing, hand off nothing, and report that the phase is waiting on manual QA plus the command that resumes it.
58
59
  - **Any QA row fails** → run `sassy-cycle` once more on the same phase, with the failing rows as the intent reference and a work brief that fixes exactly those failures. Then present the gate again. If a row still fails after that one pass, mark it `Blocked` with the reason and stop.
59
- 6. **Commit the phase.** On both gates passing, first mark the phase's QA rows `Done` with the user's notes and the phase `Done` in the Phase tracking table, then persist according to `git.repo`:
60
+ 7. **Commit the phase.** On both gates passing, first mark the phase's QA rows `Done` with the user's notes and the phase `Done` in the Phase tracking table, then persist according to `git.repo`:
60
61
 
61
- - **A remote** — dispatch a `worker` whose task is: *load the `sassy-commit` skill and follow it with `branch:current`*, naming that the commit must include the phase's code changes, the QA table results, and the now-updated Phase tracking table. It returns the hash and subject.
62
- - **`workspace`** — dispatch a `worker` per affected repo to commit that repo's code on the run's shared branch (cut once on the run's first commit with `branch:new <name>`, `branch:current` after), then run `.sassywares/workspace/snapshot` for the plan, QA results, and docs at the root. The gate's approval is the commit approval; the snapshot runs right after it and is never skipped. A missing or outdated `.sassywares/workspace/snapshot` is a blocker — say to run `sassy-setup`.
62
+ - **A remote** — dispatch a `worker` whose task is: *load the `sassy-commit` skill and follow it with `branch:current`*, naming that the commit must include the phase's code changes, the QA table results, and the now-updated Phase tracking table. It returns the hash and subject. The branch already exists: step 2 cut it, so committing on it is the point of `branch:current` here.
63
+ - **`workspace`** — dispatch a `worker` per affected repo to commit that repo's code on the run's shared branch, then run `.sassywares/workspace/snapshot` for the plan, QA results, and docs at the root. The gate's approval is the commit approval; the snapshot runs right after it and is never skipped. A missing or outdated `.sassywares/workspace/snapshot` is a blocker — say to run `sassy-setup`.
63
64
  - **`none`** — nothing is persisted. Report that the phase's work is uncommitted and stop; never hand off on top of it.
64
65
 
65
- Work commits on the current branch. `sassy-ship` creates the shipping branch at close-out when the checked-out branch is the repo base.
66
-
67
66
  Do not hand off or close out until the commit or snapshot succeeds.
68
- 7. **On commit success:**
67
+ 8. **On commit success:**
69
68
  - **A next phase exists** → hand off so it starts in a fresh session:
70
69
 
71
70
  ```text
@@ -82,16 +81,18 @@ Always gate before committing. One worker fixes findings, one re-check, then pre
82
81
 
83
82
  When the argument is not a plan path, it is one settled brief:
84
83
 
85
- 1. Run one `sassy-cycle` with the brief as the work brief and its decisions / done-when as the intent reference. Do not split it into multiple cycles.
86
- 2. Present the code approval gate with no QA checklist, and wait.
87
- 3. On code approval, persist the brief's changes the same way the plan path does: a `sassy-commit` delegation with `branch:current` in a remote repo, or the workspace snapshot in a workspace.
88
- 4. Stop and report. Shipping is a separate step (`sassy-ship`), or `sassy-lfg` would have taken it all the way. Never create or update a plan file.
84
+ 1. Resolve the branch exactly as the plan path does — `references/branching.md`, this run's `branch:` token — before the cycle runs.
85
+ 2. Run one `sassy-cycle` with the brief as the work brief and its decisions / done-when as the intent reference. Do not split it into multiple cycles.
86
+ 3. Present the code approval gate with no QA checklist, and wait.
87
+ 4. On code approval, persist the brief's changes the same way the plan path does: a `sassy-commit` delegation with `branch:current` in a remote repo, or the workspace snapshot in a workspace.
88
+ 5. Stop and report. Shipping is a separate step (`sassy-ship`), or `sassy-lfg` would have taken it all the way. Never create or update a plan file.
89
89
 
90
90
  ## Hard rules
91
91
 
92
92
  | Rule | Requirement |
93
93
  |------|-------------|
94
94
  | Orchestrator only | Delegate all implementation and quality work to `sassy-cycle`. |
95
+ | Branch before code | Resolve the branch per `references/branching.md` before the cycle runs, so no change ever lands on a base branch. |
95
96
  | One phase per run | Do exactly one phase. Never continue into the next phase in this session. |
96
97
  | Gate before commit and handoff | Do not commit, hand off, or close out until the user approves the code and the phase QA rows pass. |
97
98
  | Commit per phase | Commit the phase, its QA results, and the plan update via `sassy-commit` before handoff or close-out. |