@sassywares/skills 0.5.5 → 0.5.7
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 +10 -2
- package/package.json +1 -1
- package/skills/sassy-commit/SKILL.md +10 -3
- package/skills/sassy-commit-push-pr/SKILL.md +4 -2
- package/skills/sassy-debug/SKILL.md +8 -2
- package/skills/sassy-lfg/SKILL.md +12 -11
- package/skills/sassy-setup/SKILL.md +1 -0
- package/skills/sassy-setup/references/branching.md +132 -0
- package/skills/sassy-setup/references/config.md +50 -5
- package/skills/sassy-setup/scripts/detect.sh +229 -5
- package/skills/sassy-ship/SKILL.md +6 -3
- package/skills/sassy-work/SKILL.md +19 -18
package/README.md
CHANGED
|
@@ -205,7 +205,7 @@ flowchart LR
|
|
|
205
205
|
I -->|changes| H
|
|
206
206
|
I --> Q{Manual QA}
|
|
207
207
|
Q -->|fails| H
|
|
208
|
-
Q -->|passes, via sassy-work| J[Commit on
|
|
208
|
+
Q -->|passes, via sassy-work| J[Commit on the run branch and stop]
|
|
209
209
|
Q -->|passes, via sassy-lfg| K[sassy-ship]
|
|
210
210
|
K --> L{Ship review}
|
|
211
211
|
L -->|approve or mode:auto| M[Commit, PR, babysit]
|
|
@@ -215,7 +215,7 @@ Option 1 produces a durable plan with phased slices and manual QA tables. Option
|
|
|
215
215
|
|
|
216
216
|
`sassy-work` handles one phase and hands off the next. `sassy-lfg` drains the whole plan in one session. Both delegate to `sassy-cycle` and stop at one gate carrying code approval plus the manual QA checklist; nothing is `Done`, committed, or shipped until both pass. With an in-context brief there are no QA rows, so the gate is code approval alone.
|
|
217
217
|
|
|
218
|
-
|
|
218
|
+
The branch is cut once, before the first change, by `sassy-work` or `sassy-lfg` — never at commit time — and `sassy-ship` never creates it. `commit.branch` (default `new`) decides whether the run cuts one at all; see [Branching](#branching).
|
|
219
219
|
|
|
220
220
|
`sassy-ship` is the close-out. It summarizes what is about to ship and waits, unless the caller passed `mode:auto`.
|
|
221
221
|
|
|
@@ -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
|
package/package.json
CHANGED
|
@@ -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`.
|
|
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
|
-
**
|
|
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,12 @@ 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,
|
|
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.
|
|
44
|
+
|
|
45
|
+
With `branch:current`, commit on the checked-out branch — but check what it is first with the `sassy-setup` skill's `scripts/detect.sh --branch`. **Base means any `base_map` value, not just `origin/HEAD`.** Resolve the two facts before committing:
|
|
46
|
+
- `state: on-base` with `policy: new` means the caller skipped the branch decision. **Stop and report** — do not commit. A warning inside a subagent's summary has already failed to protect a shared branch once; this is a stop, not a notice.
|
|
47
|
+
- `commit-here` means the config or a token explicitly chose to commit here. Proceed, and name the branch in the summary you return.
|
|
48
|
+
- Any other state: commit on the checked-out branch.
|
|
42
49
|
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
50
|
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
51
|
5. **Message** — imperative subject naming the outcome, not the file list. Body only when the motivation is not obvious.
|
|
@@ -16,7 +16,9 @@ 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`.
|
|
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
|
+
|
|
21
|
+
**Which branch is a base.** Use the `sassy-setup` skill's `scripts/detect.sh --branch`: a base is any `base_map` value, not `origin/HEAD`. Probing `origin/HEAD` alone calls `staging` "not a base" in a repo whose `base_map` routes `"*"` there, which is how a run once committed phase work straight onto a shared integration branch.
|
|
20
22
|
|
|
21
23
|
**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
24
|
|
|
@@ -37,7 +39,7 @@ gh pr list --head <branch> --state open --json url,number
|
|
|
37
39
|
|
|
38
40
|
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
41
|
|
|
40
|
-
**Branch policy.**
|
|
42
|
+
**Branch policy.** The run's branch is normally already decided: `sassy-work` and `sassy-lfg` cut it at the start via the `sassy-setup` skill's `references/branching.md`, and `sassy-ship` forwards `branch:current`. So the default here is to commit on the checked-out branch when `state` is not `on-base`. Only when it *is* `on-base` or `detached` — 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
43
|
|
|
42
44
|
**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
45
|
|
|
@@ -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.
|
|
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. **
|
|
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. A `stop-and-ask` here stops the run before any phase starts.
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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.
|
|
67
|
-
2.
|
|
68
|
-
3.
|
|
65
|
+
1. Resolve the branch exactly as the plan path does — the `sassy-setup` skill's `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 the `sassy-setup` skill's `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.
|
|
@@ -0,0 +1,132 @@
|
|
|
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
|
+
Run the `sassy-setup` skill's `scripts/detect.sh` — by skill name, never by an
|
|
10
|
+
absolute install path, because a consumer's copy may live in `.agents/skills/`
|
|
11
|
+
or anywhere else a harness puts it:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bash scripts/detect.sh --branch [--policy infer|current|new] [--name BRANCH]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`--policy` is the `branch:` token: `infer` (the default), `current`, or `new`.
|
|
18
|
+
Omit it to let `commit.branch` decide; `branch:infer` means exactly that and is
|
|
19
|
+
passed only to be explicit. `--name` is optional and answers one question early:
|
|
20
|
+
which base a PR from the branch you are about to name will target.
|
|
21
|
+
|
|
22
|
+
The detector is read-only, but not network-free: when the config names no base it
|
|
23
|
+
falls back to the remote HEAD, which may shell out to `gh`.
|
|
24
|
+
|
|
25
|
+
## Output — repo topology
|
|
26
|
+
|
|
27
|
+
| Key | Meaning |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `note` | present only when the config is unusable, e.g. a bad `commit.branch`; it names the fallback taken |
|
|
30
|
+
| `topology` | `repo`, `workspace`, or `none` |
|
|
31
|
+
| `config` | the config file in force, or `none` |
|
|
32
|
+
| `configured` | `commit.branch` from the config (after validation), or `none` |
|
|
33
|
+
| `policy` | what actually applies: the token, else the config, else `new` |
|
|
34
|
+
| `checked-out` | printed only with `--head`, when you asked about a branch you are not on |
|
|
35
|
+
| `head` | the branch being decided for |
|
|
36
|
+
| `state` | `on-base`, `on-branch`, `detached`, or `no-git` |
|
|
37
|
+
| `action` | `create`, `reuse`, `commit-here`, `stop-and-ask`, or `refuse` |
|
|
38
|
+
| `start-point` | where a new branch is cut from — `<remote>/<head>`, or `none` |
|
|
39
|
+
| `unpublished` | commits on local `<head>` missing upstream, `0`, `unknown`, or `n/a` |
|
|
40
|
+
| `pr-base` | the base a PR from this run targets |
|
|
41
|
+
| `pr-base-source` | `config`, `probed` (confirm it with the user), or `unknown` |
|
|
42
|
+
| `pr-base-final` | `yes` when `--name` resolved it for the real branch name, else `no` |
|
|
43
|
+
|
|
44
|
+
A base branch is **any `base_map` value**, plus the `repos` rung, plus the remote
|
|
45
|
+
default. That is why `staging` counts as a base in a repo whose `base_map` routes
|
|
46
|
+
`"*"` to it: the remote default alone is not the base, it is only one of them.
|
|
47
|
+
|
|
48
|
+
## Token authorship
|
|
49
|
+
|
|
50
|
+
Pass `--policy` only when the **caller** asked for it — the user typed
|
|
51
|
+
`branch:new`, or the run's own gate summary already recorded the decision.
|
|
52
|
+
|
|
53
|
+
Never pass a token that is merely this skill's own default. Doing that launders
|
|
54
|
+
a default into "explicit token", which outranks the project's config and makes
|
|
55
|
+
`commit.branch` unreachable. Omitting `--policy` is how a skill defers to
|
|
56
|
+
config. This applies to every token these skills forward, not just `branch`.
|
|
57
|
+
|
|
58
|
+
## Act on `action`
|
|
59
|
+
|
|
60
|
+
### `create`
|
|
61
|
+
|
|
62
|
+
1. Run `git fetch <remote> <head>` **before** trusting the detector: a stale
|
|
63
|
+
upstream ref reads as "unpublished commits" and produces a false stop.
|
|
64
|
+
2. `unpublished` must be `0` and `start-point` must not be `none`. Anything else
|
|
65
|
+
is a stop: cutting from local HEAD would carry commits stranded on the base
|
|
66
|
+
into the new branch, and cutting from a missing upstream ref silently does
|
|
67
|
+
exactly that. Name the count, the branch, and the choice.
|
|
68
|
+
3. Name it `<type>/<slug>` from the plan filename or the topic, deterministically,
|
|
69
|
+
so a retried run converges on the same name instead of forking `-2`.
|
|
70
|
+
4. Re-run with `--name <name>` and take `pr-base` from that answer: a name like
|
|
71
|
+
`release/1.2` or `hotfix/x` matches a `base_map` family and retargets the PR,
|
|
72
|
+
and it also changes which `hooks.pr` pattern applies — a plan slug containing
|
|
73
|
+
"release" is not a neutral name.
|
|
74
|
+
5. If the name already exists: reuse it when it descends from `start-point`;
|
|
75
|
+
stop and ask when it has diverged.
|
|
76
|
+
6. `git checkout -b <name> <start-point>`. Uncommitted work comes along — say so
|
|
77
|
+
in the gate summary if the tree was dirty.
|
|
78
|
+
7. Report the branch, the start point, and `pr-base`.
|
|
79
|
+
|
|
80
|
+
### `reuse`
|
|
81
|
+
|
|
82
|
+
Report `head` and carry on. Do **not** re-cut because `commit.branch` is `new`:
|
|
83
|
+
a second phase, a resumed run, or a handed-off session is already standing on
|
|
84
|
+
the branch the first one cut. That is the whole reason this is inferred rather
|
|
85
|
+
than forced.
|
|
86
|
+
|
|
87
|
+
### `commit-here`
|
|
88
|
+
|
|
89
|
+
Only reachable with `policy: current`, which is an explicit statement that
|
|
90
|
+
committing on the base branch is intended. Proceed, and say in the run's gate
|
|
91
|
+
summary that the commits are landing on `<head>` and that `pr-base` is
|
|
92
|
+
`<pr-base>`. Never infer this from a skill default.
|
|
93
|
+
|
|
94
|
+
### `stop-and-ask`
|
|
95
|
+
|
|
96
|
+
The base has commits that never reached the remote. Present the choice — publish
|
|
97
|
+
them, move them to the new branch, or drop them — and take the answer. Never
|
|
98
|
+
resolve it by cutting anyway.
|
|
99
|
+
|
|
100
|
+
### `refuse`
|
|
101
|
+
|
|
102
|
+
Detached HEAD without a policy that permits it, `topology: none`, `git.repo:
|
|
103
|
+
none`, or no upstream ref to cut from. Report the `reason` line and stop; there
|
|
104
|
+
is no branch to commit to.
|
|
105
|
+
|
|
106
|
+
## Workspace
|
|
107
|
+
|
|
108
|
+
Resolve once at the workspace root; the run uses **one branch name across every
|
|
109
|
+
nested repo**. The detector prints one line per repo:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
repo fe: head=staging state=on-base base=staging start-point=origin/staging unpublished=0 action=create
|
|
113
|
+
drift fe: commit.branch=current
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`base` is that repo's own `base_map["*"]` when it carries a config, else its
|
|
117
|
+
`repos[<dir>]` rung. Apply `action` per repo — cut where it is `create`, keep the
|
|
118
|
+
rest — and stop if any repo reports `stop-and-ask` or `refuse`. A nested repo
|
|
119
|
+
carrying its own `commit.branch` that disagrees is printed as `drift` — a config
|
|
120
|
+
bug to report, not a per-repo override, because one run cannot follow two
|
|
121
|
+
policies.
|
|
122
|
+
|
|
123
|
+
## After the branch exists
|
|
124
|
+
|
|
125
|
+
Downstream steps forward `branch:current` to `sassy-commit`, because the decision
|
|
126
|
+
has already been made and the commits belong on that branch. `sassy-ship` never
|
|
127
|
+
creates the branch — by the time it runs, the branch carries the work. If it ever
|
|
128
|
+
finds the run's head on a base branch with unpublished commits, that is a
|
|
129
|
+
mis-run: stop and ask, never relocate commits silently.
|
|
130
|
+
|
|
131
|
+
Report the branch and its start point in every gate summary. A decision the user
|
|
132
|
+
cannot see is a decision they cannot correct.
|
|
@@ -25,6 +25,21 @@ 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.** Three sources qualify:
|
|
29
|
+
|
|
30
|
+
1. the user typed it in the request;
|
|
31
|
+
2. an approval gate earlier in the same run authorized that side effect;
|
|
32
|
+
3. the run's own orchestration decision, already recorded in its gate summary —
|
|
33
|
+
for example `sassy-ship` passing `babysit:off` because its step 6 owns the
|
|
34
|
+
babysit, or `sassy-work` forwarding `branch:current` because its step 2 cut
|
|
35
|
+
the branch and the gate showed it.
|
|
36
|
+
|
|
37
|
+
What never qualifies is **this skill's own default**. Once a default sits in the
|
|
38
|
+
slot, nothing downstream can tell it apart from the user's choice, so it
|
|
39
|
+
silently outranks the project's config. A skill defers to config by **omitting**
|
|
40
|
+
the token, never by passing a value it invented. That is how `commit.branch`
|
|
41
|
+
became unreachable while every skill still read as if it explained the rule.
|
|
42
|
+
|
|
28
43
|
Read it once per run. An orchestrator resolves every config-backed value and
|
|
29
44
|
passes the resolved values into subagent briefs; subagents never re-read the
|
|
30
45
|
file. A skill invoked directly resolves it itself.
|
|
@@ -50,7 +65,7 @@ git:
|
|
|
50
65
|
repos: # workspace only: sibling dir -> PR base
|
|
51
66
|
fe: dev
|
|
52
67
|
commit:
|
|
53
|
-
branch:
|
|
68
|
+
branch: new # current | new
|
|
54
69
|
pr:
|
|
55
70
|
babysit: true
|
|
56
71
|
merge: false
|
|
@@ -76,7 +91,7 @@ hooks:
|
|
|
76
91
|
| `git.remote` | mirror of `git.repo` | Workspace only: mirror remote (URL or `owner/name`) |
|
|
77
92
|
| `git.mirror_branch` | `main` | Workspace only: the branch the mirror pushes to |
|
|
78
93
|
| `repos` | `{}` | Workspace only: repo dir name → the base its PRs target |
|
|
79
|
-
| `commit.branch` | `
|
|
94
|
+
| `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
95
|
| `pr.babysit` | `true` | Whether close-out babysits the PR it opened |
|
|
81
96
|
| `pr.merge` | `false` | `true` authorizes merging a single babysat PR |
|
|
82
97
|
| `pr.delete_branch` | `false` | Delete the head branch once merged |
|
|
@@ -143,6 +158,35 @@ without asking, so the version line is how staleness surfaces.
|
|
|
143
158
|
In `workspace` and `none`, changes still land on disk. Nothing is silently
|
|
144
159
|
dropped; the refusal names why and what to run instead.
|
|
145
160
|
|
|
161
|
+
## `commit.branch` — decide once, before code
|
|
162
|
+
|
|
163
|
+
Resolved once per run, by the entry skill (`sassy-work`, `sassy-lfg`,
|
|
164
|
+
`sassy-debug`), **before the first change**, through
|
|
165
|
+
`sassy-setup/references/branching.md`. Not at commit time: committing first and
|
|
166
|
+
re-branching later leaves the commits on a base branch, and cutting the new
|
|
167
|
+
branch from `origin/<base>` then leaves them behind entirely.
|
|
168
|
+
|
|
169
|
+
| Value | Meaning |
|
|
170
|
+
|-------|---------|
|
|
171
|
+
| `current` | Commit where you stand, even on a base branch. An explicit statement that this repo's work in progress is acceptable |
|
|
172
|
+
| `new` (default) | Never commit on a base branch. Cut one first, then stay on it for the rest of the run |
|
|
173
|
+
|
|
174
|
+
A branch that is already checked out and is **not** a base is reused, whatever
|
|
175
|
+
the config says. That is what makes a second phase, a resumed run, or a
|
|
176
|
+
handed-off session converge on the branch the first one cut instead of cutting
|
|
177
|
+
another.
|
|
178
|
+
|
|
179
|
+
A base is **any `base_map` value**, plus the remote default — not the remote
|
|
180
|
+
default alone. In the config above `staging` is a base because `"*"` targets it,
|
|
181
|
+
even though `origin/HEAD` is `main`. Comparing only against `origin/HEAD` is how
|
|
182
|
+
phase work once got committed straight onto `staging`.
|
|
183
|
+
|
|
184
|
+
The `branch:infer|current|new` token on those skills overrides this key for one
|
|
185
|
+
run; `infer` means this key decides. In a workspace the key is resolved from the
|
|
186
|
+
workspace config for the whole run, one branch name in every repo; a nested
|
|
187
|
+
repo whose own `commit.branch` disagrees is drift, because one run cannot follow
|
|
188
|
+
two policies.
|
|
189
|
+
|
|
146
190
|
## Resolution chains
|
|
147
191
|
|
|
148
192
|
**Effective repo.** Inside a git work tree, the nearest toplevel is the repo —
|
|
@@ -186,9 +230,10 @@ config to. When a repo has both, the nested config wins and `--check` reports
|
|
|
186
230
|
the disagreement rather than picking silently. A nested config needs no ladder
|
|
187
231
|
of its own unless its PRs target more than one branch.
|
|
188
232
|
|
|
189
|
-
**Branch name**, one per run. Derived once from the plan slug or topic
|
|
190
|
-
|
|
191
|
-
|
|
233
|
+
**Branch name**, one per run. Derived once from the plan slug or topic when the
|
|
234
|
+
run starts, then used in every repo. The branch is cut at the start of the run,
|
|
235
|
+
not at commit time, so every repo sits on it before any change exists; each
|
|
236
|
+
repo cuts from its own upstream base and reuses the name in the others.
|
|
192
237
|
|
|
193
238
|
Cut from the base, then verify:
|
|
194
239
|
|
|
@@ -7,12 +7,22 @@
|
|
|
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.
|
|
16
|
+
# --name BRANCH resolves pr-base for the name a run is about to pick, because a
|
|
17
|
+
# new name can match a base_map family (release/*) and retarget the PR.
|
|
11
18
|
set -euo pipefail
|
|
12
19
|
|
|
13
20
|
ROOT_ARG=""
|
|
14
21
|
HEAD_ARG=""
|
|
22
|
+
POLICY_ARG=""
|
|
23
|
+
NAME_ARG=""
|
|
15
24
|
CHECK=0
|
|
25
|
+
BRANCH=0
|
|
16
26
|
|
|
17
27
|
while (( $# )); do
|
|
18
28
|
case "$1" in
|
|
@@ -20,8 +30,13 @@ while (( $# )); do
|
|
|
20
30
|
--root=*) ROOT_ARG="${1#*=}" ;;
|
|
21
31
|
--head) shift; [[ $# -ge 1 ]] || { echo "missing BRANCH after --head" >&2; exit 2; }; HEAD_ARG="$1" ;;
|
|
22
32
|
--head=*) HEAD_ARG="${1#*=}" ;;
|
|
33
|
+
--policy) shift; [[ $# -ge 1 ]] || { echo "missing POLICY after --policy" >&2; exit 2; }; POLICY_ARG="$1" ;;
|
|
34
|
+
--policy=*) POLICY_ARG="${1#*=}" ;;
|
|
35
|
+
--name) shift; [[ $# -ge 1 ]] || { echo "missing BRANCH after --name" >&2; exit 2; }; NAME_ARG="$1" ;;
|
|
36
|
+
--name=*) NAME_ARG="${1#*=}" ;;
|
|
37
|
+
--branch) BRANCH=1 ;;
|
|
23
38
|
--check) CHECK=1 ;;
|
|
24
|
-
-h|--help) sed -n '2,
|
|
39
|
+
-h|--help) sed -n '2,15p' "${BASH_SOURCE[0]}" | sed 's|^# \{0,1\}||'; exit 0 ;;
|
|
25
40
|
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
|
26
41
|
esac
|
|
27
42
|
shift
|
|
@@ -29,6 +44,9 @@ done
|
|
|
29
44
|
|
|
30
45
|
START="${ROOT_ARG:-$PWD}"
|
|
31
46
|
[[ -d "$START" ]] || { echo "not a directory: $START" >&2; exit 2; }
|
|
47
|
+
# --branch prints a decision; --check validates a config. One at a time, or the
|
|
48
|
+
# second is silently ignored.
|
|
49
|
+
(( BRANCH && CHECK )) && { echo "--branch and --check are separate modes; run them one at a time" >&2; exit 2; }
|
|
32
50
|
# Absolute, or the walk-up loop below re-reads "." forever.
|
|
33
51
|
START="$(cd "$START" && pwd -P)"
|
|
34
52
|
|
|
@@ -409,11 +427,217 @@ if (( CHECK )); then
|
|
|
409
427
|
[[ -n "$val" ]] && note " artifacts.$key: $val"
|
|
410
428
|
done
|
|
411
429
|
|
|
430
|
+
# commit.branch steers whether a run may touch a base branch, so an invalid
|
|
431
|
+
# value is reported rather than silently ignored.
|
|
432
|
+
val="$(yflat "commit${US}branch" 2>/dev/null || true)"
|
|
433
|
+
if [[ -n "$val" ]]; then
|
|
434
|
+
case "$val" in
|
|
435
|
+
current|new) note " commit.branch: $val" ;;
|
|
436
|
+
*) bad "commit.branch '$val' is not current|new" ;;
|
|
437
|
+
esac
|
|
438
|
+
fi
|
|
439
|
+
|
|
412
440
|
if (( drift )); then exit 1; fi
|
|
413
441
|
note " config matches the tree"
|
|
414
442
|
exit 0
|
|
415
443
|
fi
|
|
416
444
|
|
|
445
|
+
# --- Branch decision -------------------------------------------------------
|
|
446
|
+
# Read-only. Resolves commit.branch into the single decision the entry skills
|
|
447
|
+
# (sassy-work, sassy-lfg, sassy-debug) make before they start: cut a branch,
|
|
448
|
+
# reuse the one checked out, or commit where you stand.
|
|
449
|
+
#
|
|
450
|
+
# Every base_map VALUE counts as a base branch, not just the remote default.
|
|
451
|
+
# tap2's day-to-day base is `staging`, so comparing only against origin/HEAD
|
|
452
|
+
# declared staging "not a base" and let phase work commit onto it.
|
|
453
|
+
if (( BRANCH )); then
|
|
454
|
+
policy="${POLICY_ARG:-infer}"
|
|
455
|
+
case "$policy" in
|
|
456
|
+
infer|current|new) ;;
|
|
457
|
+
*) echo "unknown --policy: $policy (expected infer, current, or new)" >&2; exit 2 ;;
|
|
458
|
+
esac
|
|
459
|
+
configured="$(yflat "commit${US}branch" 2>/dev/null || true)"
|
|
460
|
+
note=""
|
|
461
|
+
if [[ -n "$configured" && "$configured" != "current" && "$configured" != "new" ]]; then
|
|
462
|
+
# A soft config error must not become a silently wrong branch decision.
|
|
463
|
+
note="commit.branch '$configured' is not current|new; using new"
|
|
464
|
+
configured=""
|
|
465
|
+
fi
|
|
466
|
+
if [[ "$policy" == "infer" ]]; then
|
|
467
|
+
policy="${configured:-new}"
|
|
468
|
+
fi
|
|
469
|
+
|
|
470
|
+
# base_heads_in DIR — every branch DIR's config treats as a base: each
|
|
471
|
+
# base_map value, the repos[<dir>] rung, and the remote default. A workspace
|
|
472
|
+
# has no base_map of its own, so repos[] is what names its bases; reading
|
|
473
|
+
# base_map alone reported every nested repo as on-branch and let runs commit
|
|
474
|
+
# onto staging, which is the bug this whole mode exists to stop.
|
|
475
|
+
base_heads_in() {
|
|
476
|
+
local dir="$1" r v out=""
|
|
477
|
+
if [[ -f "$dir/.sassywares/config.yaml" ]]; then
|
|
478
|
+
out="$(yaml_flat "$dir/.sassywares/config.yaml" 2>/dev/null | awk -F'\t' -v us="$US" \
|
|
479
|
+
'{n=split($1,a,us); if (n==3 && a[1]=="git" && a[2]=="base_map") print $2}' | sort -u || true)"
|
|
480
|
+
fi
|
|
481
|
+
v="$(yflat "repos${US}$(basename "$dir")" 2>/dev/null || true)"
|
|
482
|
+
[[ -n "$v" ]] && out="$out"$'\n'"$v"
|
|
483
|
+
r="$(remote_name "$dir")"
|
|
484
|
+
if [[ -n "$r" ]]; then
|
|
485
|
+
v="$(git -C "$dir" symbolic-ref --short "refs/remotes/$r/HEAD" 2>/dev/null | sed "s|^$r/||" || true)"
|
|
486
|
+
[[ -n "$v" ]] && out="$out"$'\n'"$v"
|
|
487
|
+
fi
|
|
488
|
+
out="$(printf '%s\n' "$out" | grep -v '^$' | sort -u || true)"
|
|
489
|
+
# Probing through gh costs a network call, so only when nothing else said.
|
|
490
|
+
[[ -n "$out" ]] || out="$(detect_base "$dir" 2>/dev/null || true)"
|
|
491
|
+
printf '%s\n' "$out" | grep -v '^$' || true
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
# in_base_heads DIR BRANCH — membership without a pipe: `… | grep -q` trips
|
|
495
|
+
# pipefail, because grep exits on the first match and SIGPIPEs the producer,
|
|
496
|
+
# so a branch that IS a base reported as if it were not.
|
|
497
|
+
in_base_heads() {
|
|
498
|
+
local dir="$1" want="$2" b
|
|
499
|
+
while IFS= read -r b; do
|
|
500
|
+
[[ "$b" == "$want" ]] && return 0
|
|
501
|
+
done < <(base_heads_in "$dir")
|
|
502
|
+
return 1
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
# repo_base DIR — the base a PR from DIR targets, for a brand-new branch name.
|
|
506
|
+
repo_base() {
|
|
507
|
+
local dir="$1" v="" r
|
|
508
|
+
if [[ -f "$dir/.sassywares/config.yaml" ]]; then
|
|
509
|
+
v="$(yflat_in "$dir/.sassywares/config.yaml" "git${US}base_map${US}*" 2>/dev/null || true)"
|
|
510
|
+
fi
|
|
511
|
+
[[ -n "$v" ]] || v="$(yflat "repos${US}$(basename "$dir")" 2>/dev/null || true)"
|
|
512
|
+
if [[ -z "$v" ]]; then
|
|
513
|
+
v="$(detect_base "$dir" 2>/dev/null || true)"
|
|
514
|
+
[[ -n "$v" ]] && printf '%s' "$v" && return 0
|
|
515
|
+
fi
|
|
516
|
+
printf '%s' "$v"
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
# classify DIR [HEAD] — on-base, on-branch, detached, or unknown.
|
|
520
|
+
classify() {
|
|
521
|
+
local dir="$1" h="$2"
|
|
522
|
+
if [[ -z "$h" ]]; then printf 'detached'; return 0; fi
|
|
523
|
+
if in_base_heads "$dir" "$h"; then printf 'on-base'; else printf 'on-branch'; fi
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
# start_point DIR HEAD — the upstream ref a new branch is cut from. Prints
|
|
527
|
+
# "none" when there is no upstream ref: cutting from local HEAD would carry
|
|
528
|
+
# commits stranded on the base, which is the failure this prevents.
|
|
529
|
+
start_point() {
|
|
530
|
+
local dir="$1" h="$2" r
|
|
531
|
+
r="$(remote_name "$dir")"
|
|
532
|
+
if [[ -n "$r" && -n "$h" ]] && git -C "$dir" rev-parse --verify --quiet "refs/remotes/$r/$h" >/dev/null 2>&1; then
|
|
533
|
+
printf '%s/%s' "$r" "$h"
|
|
534
|
+
else
|
|
535
|
+
printf 'none'
|
|
536
|
+
fi
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
# unpublished DIR HEAD — commits on the local base that are not upstream.
|
|
540
|
+
# Unknown on error, never 0: an unreadable count must not read as "clean".
|
|
541
|
+
unpublished() {
|
|
542
|
+
local dir="$1" h="$2" r n
|
|
543
|
+
r="$(remote_name "$dir")"
|
|
544
|
+
[[ -n "$r" && -n "$h" ]] || { printf 'unknown'; return 0; }
|
|
545
|
+
git -C "$dir" rev-parse --verify --quiet "refs/remotes/$r/$h" >/dev/null 2>&1 || { printf 'unknown'; return 0; }
|
|
546
|
+
n="$(git -C "$dir" rev-list --count "$r/$h..$h" 2>/dev/null || true)"
|
|
547
|
+
[[ -n "$n" ]] && printf '%s' "$n" || printf 'unknown'
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
# decide STATE POLICY START UNPUBLISHED — the action, fail closed.
|
|
551
|
+
decide() {
|
|
552
|
+
local st="$1" pol="$2" start="$3" unp="$4"
|
|
553
|
+
case "$st:$pol" in
|
|
554
|
+
on-branch:*) printf 'reuse'; return 0 ;;
|
|
555
|
+
on-base:new) ;;
|
|
556
|
+
on-base:current) printf 'commit-here'; return 0 ;;
|
|
557
|
+
detached:new) ;;
|
|
558
|
+
*) printf 'refuse'; return 0 ;;
|
|
559
|
+
esac
|
|
560
|
+
# Cutting needs a known-clean upstream base.
|
|
561
|
+
[[ "$start" == "none" ]] && { printf 'refuse'; return 0; }
|
|
562
|
+
[[ "$unp" == "0" ]] || { printf 'stop-and-ask'; return 0; }
|
|
563
|
+
printf 'create'
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
repo_value="$(yflat "git${US}repo" 2>/dev/null || true)"
|
|
567
|
+
|
|
568
|
+
[[ -n "$note" ]] && printf 'note: %s\n' "$note"
|
|
569
|
+
printf 'topology: %s\n' "$TOPOLOGY"
|
|
570
|
+
printf 'config: %s\n' "${CONFIG:-none}"
|
|
571
|
+
printf 'configured: %s\n' "${configured:-none}"
|
|
572
|
+
printf 'policy: %s\n' "$policy"
|
|
573
|
+
|
|
574
|
+
if [[ "$TOPOLOGY" == "none" || "$repo_value" == "none" ]]; then
|
|
575
|
+
if [[ "$repo_value" == "none" ]]; then
|
|
576
|
+
printf 'state: no-git\naction: refuse\nreason: git.repo is none\n'
|
|
577
|
+
else
|
|
578
|
+
printf 'state: no-git\naction: refuse\nreason: not a git work tree\n'
|
|
579
|
+
fi
|
|
580
|
+
exit 0
|
|
581
|
+
fi
|
|
582
|
+
|
|
583
|
+
if [[ "$TOPOLOGY" == "workspace" ]]; then
|
|
584
|
+
# One policy for the whole run, resolved per repo: a run cannot follow two
|
|
585
|
+
# branch policies, so a nested repo that disagrees is reported as drift.
|
|
586
|
+
while IFS= read -r dir; do
|
|
587
|
+
[[ -n "$dir" ]] || continue
|
|
588
|
+
d="$ROOT/$dir"
|
|
589
|
+
h="$(git -C "$d" branch --show-current 2>/dev/null || true)"
|
|
590
|
+
st="$(classify "$d" "$h")"
|
|
591
|
+
sp="$(start_point "$d" "$h")"
|
|
592
|
+
base="$(repo_base "$d")"
|
|
593
|
+
if [[ "$st" == "on-base" ]]; then unp="$(unpublished "$d" "$h")"; else unp=n/a; fi
|
|
594
|
+
printf 'repo %s: head=%s state=%s base=%s start-point=%s unpublished=%s action=%s\n' \
|
|
595
|
+
"$dir" "${h:-none}" "$st" "${base:-none}" "$sp" "$unp" "$(decide "$st" "$policy" "$sp" "$unp")"
|
|
596
|
+
own=""
|
|
597
|
+
if [[ -f "$d/.sassywares/config.yaml" ]]; then
|
|
598
|
+
own="$(yflat_in "$d/.sassywares/config.yaml" "commit${US}branch" 2>/dev/null || true)"
|
|
599
|
+
fi
|
|
600
|
+
[[ -n "$own" && "$own" != "$policy" ]] && printf 'drift %s: commit.branch=%s\n' "$dir" "$own"
|
|
601
|
+
done < <(nested_repos)
|
|
602
|
+
exit 0
|
|
603
|
+
fi
|
|
604
|
+
|
|
605
|
+
head="${HEAD_ARG:-$(git -C "$ROOT" branch --show-current 2>/dev/null || true)}"
|
|
606
|
+
actual="$(git -C "$ROOT" branch --show-current 2>/dev/null || true)"
|
|
607
|
+
[[ -n "$HEAD_ARG" && "$HEAD_ARG" != "$actual" ]] && printf 'checked-out: %s\n' "${actual:-none}"
|
|
608
|
+
printf 'head: %s\n' "${head:-none}"
|
|
609
|
+
|
|
610
|
+
state="$(classify "$ROOT" "$head")"
|
|
611
|
+
printf 'state: %s\n' "$state"
|
|
612
|
+
start="$(start_point "$ROOT" "$head")"
|
|
613
|
+
unp=n/a
|
|
614
|
+
[[ "$state" == "on-base" ]] && unp="$(unpublished "$ROOT" "$head")"
|
|
615
|
+
printf 'action: %s\n' "$(decide "$state" "$policy" "$start" "$unp")"
|
|
616
|
+
printf 'start-point: %s\n' "$start"
|
|
617
|
+
printf 'unpublished: %s\n' "$unp"
|
|
618
|
+
|
|
619
|
+
# pr-base is provisional for a branch that does not exist yet: the name the
|
|
620
|
+
# skill picks can match a base_map family (release/*, hotfix/*) and retarget
|
|
621
|
+
# the PR. Pass --name once the name is known to get the final answer.
|
|
622
|
+
pr_src=config
|
|
623
|
+
if [[ -n "$NAME_ARG" ]]; then
|
|
624
|
+
pr_base="$(base_for_head "$NAME_ARG" 2>/dev/null || true)"
|
|
625
|
+
elif [[ "$state" == "on-base" || "$state" == "detached" ]]; then
|
|
626
|
+
pr_base="$(repo_base "$ROOT")"
|
|
627
|
+
else
|
|
628
|
+
pr_base="$(base_for_head "$head" 2>/dev/null || true)"
|
|
629
|
+
fi
|
|
630
|
+
if [[ -z "$pr_base" ]]; then
|
|
631
|
+
pr_base="$(detect_base "$ROOT" 2>/dev/null || true)"
|
|
632
|
+
pr_src=probed
|
|
633
|
+
fi
|
|
634
|
+
[[ -n "$pr_base" ]] || pr_src=unknown
|
|
635
|
+
printf 'pr-base: %s\n' "$pr_base"
|
|
636
|
+
printf 'pr-base-source: %s\n' "$pr_src"
|
|
637
|
+
printf 'pr-base-final: %s\n' "$( [[ -n "$NAME_ARG" ]] && printf yes || printf no )"
|
|
638
|
+
exit 0
|
|
639
|
+
fi
|
|
640
|
+
|
|
417
641
|
# --- Emit a proposal -------------------------------------------------------
|
|
418
642
|
# Every key carries its meaning and its allowed values. The file is the
|
|
419
643
|
# documentation a teammate reads, so the comments are part of the output, not
|
|
@@ -532,12 +756,12 @@ esac
|
|
|
532
756
|
cat <<'EOF'
|
|
533
757
|
|
|
534
758
|
# --- Commit ---------------------------------------------------------------
|
|
535
|
-
# current: commit on the checked-out branch.
|
|
536
|
-
# new:
|
|
537
|
-
#
|
|
759
|
+
# current: commit on the checked-out branch, even on a base branch.
|
|
760
|
+
# new: never commit on a base branch — sassy-work, sassy-lfg, and sassy-debug
|
|
761
|
+
# cut a branch at the start of the run and stay on it.
|
|
538
762
|
commit:
|
|
539
763
|
EOF
|
|
540
|
-
kv branch
|
|
764
|
+
kv branch new 34 '# current | new'
|
|
541
765
|
cat <<'EOF'
|
|
542
766
|
|
|
543
767
|
# --- 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
|
|
30
|
-
|
|
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. Ask the commit worker to return `state` and `unpublished` from the `sassy-setup` skill's `scripts/detect.sh --branch`, and stop and ask if they show a base with a non-zero 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 |
|
|
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. A head on a base branch with unpublished commits is a stop-and-ask, decided from the commit worker's `state` and `unpublished`. |
|
|
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. |
|
|
@@ -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
|
|
50
|
-
2. **
|
|
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 the cycle and goes straight to step 5, but **still resolve the branch at step 2 first**: on `reuse` it is a no-op, and on `create` it is the difference between committing on the base and not.
|
|
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. On `stop-and-ask`, present the choice and wait. Carry the branch and its start point into every gate summary you present.
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
- **Code approved and every QA row passes** → go to step
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
86
|
-
2.
|
|
87
|
-
3.
|
|
88
|
-
4.
|
|
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 the `sassy-setup` skill's `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. |
|