dw-sdlc 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,10 @@
1
1
  # dw-sdlc
2
2
 
3
- The Gate and the agentic config of a repository. `.ai/agentic.config.json` declares how a repository
4
- works for an agent: its package manager, protected branches, env-file policy, commit convention, the
5
- Gate and who merges a pull request. The package ships the `dw-sdlc` bin that runs on that file, its JSON Schema, and the
6
- resolver of its layers.
3
+ The bin and the config behind the [dw-sdlc](https://github.com/dominikwozniak/dw-sdlc) Claude Code plugins.
4
+ `.ai/agentic.config.json` declares how a repository works for an agent: its package manager, protected
5
+ branches, env-file policy, commit convention, the Gate and who merges a pull request. The package ships
6
+ the `dw-sdlc` bin that runs on that file, the file's JSON Schema and the resolver of its layers. The Gate
7
+ and the Ship script work in any repository, with or without the plugins.
7
8
 
8
9
  ## Install
9
10
 
@@ -13,17 +14,17 @@ Node 24 or later.
13
14
  pnpm add -D dw-sdlc
14
15
  ```
15
16
 
17
+ Every release goes to npm from GitHub Actions by trusted publishing, and every version after 0.17.0
18
+ carries provenance too. Trust evidence never drops from one version to the next, so a repository on
19
+ pnpm's `trustPolicy: no-downgrade` takes each release. Dependabot offers one after the seven days of
20
+ `minimumReleaseAge`; a repository that cannot wait names `dw-sdlc` in `minimumReleaseAgeExclude` under
21
+ its own rule for exclusions.
22
+
16
23
  ## The Gate
17
24
 
18
- `dw-sdlc gate` runs the Steps under `gate.steps` in order from the repository root, then every Check
19
- under `gate.checks`. It prints one line per Step and per Check, and the last 50 lines of a command
20
- that fails; `--verbose` streams every command's output instead. A Step that creates, changes or
21
- deletes a file git tracks or would track fails the Gate as Drift, unless `gate.drift` is `"allow"`.
22
- It exits 0 when everything passes, 1 when a Step or a Check fails or a Step drifts, and 2 when the
23
- Gate cannot run. `--json` prints the result as one document. `--cache` skips the run when the
24
- working tree and the resolved Gate match the last green run in that checkout, so a pre-push hook
25
- costs nothing on an unchanged tree. `dw-sdlc check <name>` runs one Check alone, with the same
26
- lines, exit codes and document.
25
+ `dw-sdlc gate` proves a change. It runs the Steps under `gate.steps`, the repository's own commands, in
26
+ order from the repository root, stopping at the first that fails. Then it runs every Check under
27
+ `gate.checks`, a named rule the package ships, none stopping the next.
27
28
 
28
29
  ```json
29
30
  {
@@ -35,90 +36,113 @@ lines, exit codes and document.
35
36
  }
36
37
  ```
37
38
 
38
- A Step is a command string, or `{ "command": "…", "hint": "…" }`; a `gate.checks.run` entry takes a
39
- `hint` beside `name` and `command`. When the Step or script fails, the Gate prints `hint: <text>` under
40
- its failure line, and `--json` carries `hint` on every Step and Check, `null` where there is none.
41
-
42
39
  ```json
43
40
  { "scripts": { "check": "dw-sdlc gate" } }
44
41
  ```
45
42
 
46
- Under `gate.checks`, a Check the package ships is `true` for its defaults or an object of its options.
47
- `comment-budget` holds each comment block in the JavaScript, TypeScript and shell (`.sh`) files under
48
- its `roots` to `maxProseLines` lines of prose, 3 by default.
49
- `disable-directives` fails every Disable directive in a comment that gives no reason: `oxlint-disable*`,
50
- `eslint-disable*`, `@ts-expect-error`, `@ts-ignore`, `rubocop:disable` and `noqa`. A reason follows `--`
51
- on the directive's line, follows an `@ts-` directive, or is a comment on the line above.
52
-
53
- `skill-sections` holds each README section a skill copies to its source. A part sits between two
54
- markers, each on its own line, and `dw-sdlc check skill-sections --write` fills an empty pair:
55
-
56
- ```md
57
- <!-- README.md#errors -->
58
- <!-- /README.md#errors -->
59
- ```
60
-
61
- The anchor is GitHub's, and the section runs from its heading to the next heading of its level or
62
- above, in the `README.md` beside the `skills` directory that holds the file, or beside the file
63
- outside one. The Check fails a part that differs from its section, a part whose anchor names no
64
- heading of the README, a part from a section no Primitive in `.ai/primitives.json` names among its
65
- docs, a marker without its pair, and a code fence that never closes above a marker, which hides it.
43
+ It prints one line per Step and per Check, and the last 50 lines of a command that fails. A Step that
44
+ creates, changes or deletes a file git tracks or would track fails as Drift, unless `gate.drift` is
45
+ `"allow"`. A Step written as `{ "command": "…", "hint": "…" }` prints `hint: <text>` under its failure
46
+ line; a `gate.checks.run` entry takes a `hint` beside `name` and `command`.
47
+
48
+ | flag | does |
49
+ | ------------------- | --------------------------------------------------------------------------------- |
50
+ | `--json` | prints the result as one document; each Step and Check carries `tail` and `hint` |
51
+ | `--verbose` | streams every command's output instead of the tail |
52
+ | `--cache` | skips the run when the tree and the Gate match the last green run in the checkout |
53
+ | `--update-baseline` | after a green run, tightens the baseline of each Ratchet |
54
+
55
+ | exit | means |
56
+ | ---- | ---------------------------------------------------------------------------------- |
57
+ | 0 | everything passes |
58
+ | 1 | a Step or a Check fails, or a Step drifts |
59
+ | 2 | the Gate cannot run: an invalid config layer, a command not found, a missing input |
60
+
61
+ `dw-sdlc check <name>` runs one Check alone, with the same lines, exit codes and document.
62
+
63
+ ### Checks
64
+
65
+ Under `gate.checks`, a Check is `true` for its defaults, an object of its options, or `false`. The
66
+ schema's description of each one names its options and defaults.
67
+
68
+ | Check | fails | reads first |
69
+ | -------------------- | --------------------------------------------------------------------------- | ------------------------- |
70
+ | `markdown-refs` | a relative link or image whose path or heading anchor does not resolve | nothing |
71
+ | `temporal-language` | a changelog phrase in a durable document, outside code | nothing |
72
+ | `comment-budget` | a comment block over 3 lines of prose in JS, TS or shell | nothing |
73
+ | `disable-directives` | a linter or type-checker disable comment with no reason | nothing |
74
+ | `agents-map` | an `AGENTS.md` over budget, missing the Gate command or pointing at nothing | `AGENTS.md` |
75
+ | `primitives` | a catalogue entry out of step with the tree, or a file no entry owns | `.ai/primitives.json` |
76
+ | `routed-docs` | a document under `docs/` no Task Router row or Primitive leads to | the map, the catalogue |
77
+ | `markdown-budget` | a markdown file over the byte budget of its glob | the files its globs match |
78
+ | `decision-records` | a decision record misnamed, misnumbered or without a valid status | a records directory |
79
+ | `file-size-ratchet` | a code file over its line limit that the baseline does not allow | its baseline |
80
+ | `skill-sections` | a README section a skill copies that differs from its source | the catalogue |
81
+
82
+ Each Check's options and defaults:
83
+ [its description in the schema](https://github.com/dominikwozniak/dw-sdlc/blob/main/packages/dw-sdlc/config/agentic.config.schema.json).
66
84
 
67
85
  ## The Ship script
68
86
 
69
- `dw-sdlc pr ship [<n>] [-R owner/name]` reads one pull request through an authenticated `gh` and
70
- answers the one Action it needs next, as a line or, with `--json`, one document. The exit code says
71
- whose turn it is: 0 done, 10 the agent's, 20 wait, 30 the dev's, 2 it cannot run. The script
72
- performs its own Actions: it updates a branch GitHub reports `BEHIND`, marks the head reviewed when the
73
- Review depth is `off`, marks a draft ready once nothing else holds it back, and squash merges a pull
74
- request that is green, reviewed and opened by the `gh` account, when the merge is its own. `--dry-run` performs nothing: it answers the first Action the
75
- script would perform as the dev's. It never deletes a branch; a merged pull request answers with
76
- the delete command for the dev.
77
-
78
- `pr.merge` in the committed `.ai/agentic.config.json` says who merges: the script under `auto`, the
79
- dev under `ask`, the default. `pr.review` says how closely a head no one has reviewed is read: the
80
- agent reviews it under `full`, one `sdlc-reviewer` subagent of the plugin does under `light`, and no
81
- one does under `off`. Under `auto`, the default, the script routes the depth from the changes:
82
- lockfiles alone `off`; a workflow file, more than 100 changed lines outside lockfiles, or a change
83
- too large to size `full`; the rest `light`. The local layer may tighten either and never loosen it;
84
- `--merge` acts as `auto` and `--review full|light|off` sets the depth for one call. A pull request
85
- the `gh` account did not open is always the dev's to merge.
87
+ `dw-sdlc pr ship [<n>] [-R owner/name]` reads one pull request through an authenticated `gh` and answers
88
+ the one Action it needs next, as a line or, with `--json`, one document. It performs its own Actions:
89
+ it updates a branch GitHub reports `BEHIND`, marks the head reviewed at Review depth `off`, marks a draft
90
+ ready once nothing else holds it back, and squash merges when the merge is its own. It never deletes a
91
+ branch. `--dry-run` performs nothing.
92
+
93
+ | exit | whose turn |
94
+ | ---- | ------------------ |
95
+ | 0 | done |
96
+ | 10 | the agent's: act |
97
+ | 20 | nobody's yet: wait |
98
+ | 30 | the dev's: park |
99
+ | 2 | it cannot run |
100
+
101
+ | key | sets |
102
+ | ------------------ | ---------------------------------------------------------------------------------------------- |
103
+ | `pr.merge` | who merges: the script under `auto`, the dev under `ask`, the default |
104
+ | `pr.review` | the Review depth of an unreviewed head: `full`, `light`, `off`, or `auto` from the change size |
105
+ | `pr.reviewCommand` | the slash command a `full` Review runs, `/code-review` by default |
86
106
 
87
107
  ```json
88
108
  { "pr": { "merge": "auto", "review": "full" } }
89
109
  ```
90
110
 
91
- `pr ship` answers `review`, its depth under `details`, until the head carries the commit status
92
- `dw-sdlc/review`, which `dw-sdlc pr reviewed <n> --head <sha>` posts after a clean review. A Review
93
- that leaves a Finding open posts it with `--findings`, in state failure, "read, findings open": the
94
- read mark. After the next push, `details.since` names the last head a Review read, clean or marked,
95
- so the Review after a fix reads the fix alone. `pr reviewed` posts nothing and exits 2 once a push
96
- moved the head, so a new head needs a fresh review. Under `off` the script posts that status
97
- itself, its description saying why, and goes on.
111
+ The local layer may tighten `pr.merge` and `pr.review`, never loosen them. `--merge` acts as `auto` and
112
+ `--review full|light|off` sets the depth for one call. A pull request the `gh` account did not open is
113
+ always the dev's to merge. `light` spawns the `sdlc-reviewer` subagent of the
114
+ [`sdlc` plugin](https://github.com/dominikwozniak/dw-sdlc/blob/main/plugins/sdlc/README.md#the-light-review).
98
115
 
99
- Three Reviews with no success between them end the loop: `pr ship` answers `park review-limit`
100
- instead of `review`, whose command, `dw-sdlc pr reviewed <n> -R <owner/name> --head <sha> --accept`,
101
- is the dev's. `--accept` posts the success described "accepted by the dev, findings open"; with
102
- `--findings` it exits 2. The limit is a constant, not a config key, and `--review <depth>` on a call
103
- buys one more Review past it.
116
+ `dw-sdlc pr reviewed <n> --head <sha>` pins a Review to the head it read, as the commit status
117
+ `dw-sdlc/review`: clean, Findings open with `--findings`, or accepted by the dev with `--accept`. The
118
+ routing rules, the read mark and the limit of three unclean Reviews are in
119
+ [How the Ship script merges a pull request](https://github.com/dominikwozniak/dw-sdlc/blob/main/docs/overview.md#how-the-ship-script-merges-a-pull-request).
104
120
 
105
121
  ## Commands
106
122
 
107
- | command | what it does |
108
- | ------------------ | ------------------------------------------------------------------------------------------ |
109
- | `gate` | the Steps of `gate.steps` in order, then every Check under `gate.checks` |
110
- | `check <name>` | one Check of the Gate by its name, without the Steps and the other Checks |
111
- | `primitives` | the Primitives that own each path, or that a change removes, adds or extends |
112
- | `setup` | this machine's local config layer, the git excludes and the permission rules |
113
- | `doctor` | one PASS / WARN / FAIL line per machine check the guardrails rely on |
114
- | `config` | the resolved agentic config, or one file validated against the schema |
115
- | `references sync` | every declared Reference brought to its ref under `references/` |
116
- | `references setup` | the Router, its table and the ignore rules a repository needs to declare References |
117
- | `pr ship [<n>]` | the Action one pull request needs next, performing the script's own |
118
- | `pr reviewed <n>` | a Review pinned to the head it read, as a commit status: clean, Findings open, or accepted |
119
- | `recap` | the facts a Recap rests on and whether the session can be closed |
120
-
121
- `dw-sdlc <command> --help` gives each one's options.
123
+ | command | what it does |
124
+ | ------------------ | ------------------------------------------------------------------------------------------------- |
125
+ | `gate` | the Gate: the Steps of `gate.steps` in order, then every Check under `gate.checks` |
126
+ | `check <name>` | one Check of the Gate by its name, without the Steps and the other Checks |
127
+ | `primitives` | the Primitives that own each path, or that a change removes, adds or extends |
128
+ | `setup` | the Gate adopted where asked, this machine's local config layer, git excludes and permissions |
129
+ | `doctor` | one PASS / WARN / FAIL line per machine check the `sdlc` plugin relies on |
130
+ | `config` | the resolved agentic config, or one file validated against the schema |
131
+ | `references sync` | every declared Reference, a repository cloned for reading, brought to its ref under `references/` |
132
+ | `references setup` | the Router, its table and the ignore rules a repository needs to declare References |
133
+ | `pr ship [<n>]` | the Action one pull request needs next, performing the script's own |
134
+ | `pr reviewed <n>` | a Review pinned to the head it read, as a commit status |
135
+ | `recap` | the facts a Recap rests on, whether the session can be closed and what could come next |
136
+ | `sweep` | Landed branches and clean worktrees removed, and what it keeps named |
137
+ | `worker start` | a Worker started on a Brief in a herdr terminal |
138
+ | `worker ls` | the Workers this Lead started and their states |
139
+ | `worker report` | a Recap delivered to the Lead, retried while it is blocked |
140
+ | `lead start` | the next session started as a Lead in a herdr terminal of its own |
141
+
142
+ `dw-sdlc --help` lists them, and `dw-sdlc <command> --help` gives each one's options and exit codes. A
143
+ Primitive is one unit a repository exposes to an agent, a module, a hook, a script, a config key or a
144
+ skill, listed in `.ai/primitives.json`. A Recap, a Brief, a Worker and a Lead belong to the
145
+ [`sdlc-sessions` plugin](https://github.com/dominikwozniak/dw-sdlc/blob/main/plugins/sdlc-sessions/README.md).
122
146
 
123
147
  ## The resolver
124
148
 
@@ -129,8 +153,8 @@ const { config, problems } = resolveConfig(process.cwd())
129
153
  ```
130
154
 
131
155
  `resolveConfig` merges the committed `.ai/agentic.config.json`, the machine's
132
- `.ai/agentic.config.local.json` and what it infers from the repository. `problems` names each layer
133
- that fails the schema; an invalid layer is dropped whole.
156
+ `.ai/agentic.config.local.json` and what it infers from the repository. `problems` names each layer that
157
+ fails the schema; an invalid layer is dropped whole, and the guards then run the strictest set.
134
158
 
135
159
  ## License
136
160
 
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://raw.githubusercontent.com/dominikwozniak/dw-sdlc/main/plugins/guardrails/config/agentic.config.schema.json",
3
+ "$id": "https://raw.githubusercontent.com/dominikwozniak/dw-sdlc/main/packages/dw-sdlc/config/agentic.config.schema.json",
4
4
  "title": "agentic.config",
5
- "description": "How a repository works for an agent. `.ai/agentic.config.json` is committed; `.ai/agentic.config.local.json` overrides it on one machine and may tighten `guardrails` and `pr.merge` but never loosen them. A key neither file sets is inferred: the Node package manager from the lockfile, the `conventional` commit preset, and `gate.command` from the manifest's `check` script. Unknown top-level keys are tolerated so other tool families can share the file; the keys named here refuse unknown children.",
5
+ "description": "How a repository works for an agent. `.ai/agentic.config.json` is committed; `.ai/agentic.config.local.json` overrides it on one machine and may tighten `guardrails`, `pr.merge` and `pr.review` but never loosen them. A key neither file sets is inferred: the Node package manager from the lockfile, the `conventional` commit preset, and `gate.command` from the manifest's `check` script. Unknown top-level keys are tolerated so other tool families can share the file; the keys named here refuse unknown children.",
6
6
  "type": "object",
7
7
  "properties": {
8
8
  "$schema": {
@@ -47,7 +47,11 @@ function checkJq(probe) {
47
47
  if (status === 0) {
48
48
  return { name: "jq", level: "PASS", detail: stdout.trim() };
49
49
  }
50
- return { name: "jq", level: "FAIL", detail: "missing: every guard is silent without it. brew install jq" };
50
+ return {
51
+ name: "jq",
52
+ level: "FAIL",
53
+ detail: "missing: every guard is silent without it. https://jqlang.org/download/"
54
+ };
51
55
  }
52
56
  /** Every hook command string in a settings file, whatever the event and matcher. */
53
57
  function hookCommands(settings) {
@@ -27,8 +27,8 @@ export type SetupOptions = {
27
27
  export declare const summary = "adopt the Gate where asked, and write this machine's local config layer, the git excludes and the permission rules";
28
28
  /**
29
29
  * What the machine's layer points `$schema` at: the marketplace clone's schema as a `file://` URL, as an
30
- * editor cannot fetch the `$id` from a private repository and the clone, unlike the install cache, keeps
31
- * its path across plugin updates. Without a registered clone holding the schema, the `$id`.
30
+ * editor reads it offline at the installed version and the clone, unlike the install cache, keeps its
31
+ * path across plugin updates. Without a registered clone holding the schema, the `$id`.
32
32
  */
33
33
  export declare function schemaReference(claudeDir: string): string;
34
34
  export declare function setup(options: SetupOptions): Step[];
@@ -69,8 +69,8 @@ function committedConfig(root) {
69
69
  }
70
70
  /**
71
71
  * What the machine's layer points `$schema` at: the marketplace clone's schema as a `file://` URL, as an
72
- * editor cannot fetch the `$id` from a private repository and the clone, unlike the install cache, keeps
73
- * its path across plugin updates. Without a registered clone holding the schema, the `$id`.
72
+ * editor reads it offline at the installed version and the clone, unlike the install cache, keeps its
73
+ * path across plugin updates. Without a registered clone holding the schema, the `$id`.
74
74
  */
75
75
  export function schemaReference(claudeDir) {
76
76
  const registry = readJson(join(claudeDir, "plugins", "known_marketplaces.json"));
@@ -9,7 +9,7 @@ import { isCommitLanded, isLanded } from "../landed.js";
9
9
  import { shellLine } from "../pr/decide.js";
10
10
  import { CannotRun } from "../pr/read.js";
11
11
  import { byHand, holdersOf, keptIn, lockAfterStop, lockHolder, outsideWorktrees, readTerminals, removeTerminal, stopsOf, sweepingTerminal, whyHeld } from "../sweep-terminals.js";
12
- import { dirtyFiles, must, pausedOperation, worktreesOf } from "../sweep-trees.js";
12
+ import { branchesOf, dirtyFiles, pausedOperation, switchOff, worktreesOf } from "../sweep-trees.js";
13
13
  import { asked, git } from "../working-tree.js";
14
14
  import { lockVerdict } from "../worktree-lock.js";
15
15
  export const summary = "removes Landed branches and clean worktrees whose work has landed, names what it keeps";
@@ -26,7 +26,8 @@ config, else the branch origin/HEAD names) and removes, never forcing:
26
26
  It keeps, and names with the reason and the command to run yourself:
27
27
  a worktree that fails the set: the files that hold it with the forced removal, the paused operation, the
28
28
  lock's reason with git worktree unlock, a live Claude Code lock named by its pid, a HEAD that is not Landed;
29
- the default branch, the base branch, a protected branch and a branch a kept worktree has checked out;
29
+ the default branch, the base branch, a protected branch and a branch a kept worktree has checked out; a
30
+ Landed branch the main checkout has checked out, clean and with nothing paused, with git switch to the base;
30
31
  a branch whose work is on no commit of the base, with the forced delete.
31
32
  It never changes anything on a remote: the fetch only writes remote-tracking refs. A removal git refuses is
32
33
  kept with git's words, and the sweep goes on.
@@ -91,11 +92,6 @@ function baseOf(root, head) {
91
92
  }
92
93
  return { name, ref };
93
94
  }
94
- function branchesOf(root) {
95
- return must(root, ["for-each-ref", "--format=%(refname:lstrip=2)", "refs/heads"])
96
- .split("\n")
97
- .filter((name) => name !== "");
98
- }
99
95
  /** Why the worktree stays, with the dev's command when there is one, or null when its whole set passes. */
100
96
  function whyTreeStays(root, baseRef, tree, lock, holders) {
101
97
  const { path } = tree;
@@ -194,8 +190,9 @@ function judge(root, terminals) {
194
190
  }
195
191
  judged.add(name);
196
192
  const why = stays(name, footing);
193
+ const off = why?.dev === true && held.get(name) === checkout.path ? switchOff(root, base, name) : null;
197
194
  if (why !== null) {
198
- kept.push({ kind: "branch", name, ...why });
195
+ kept.push({ kind: "branch", name, ...why, ...off });
199
196
  }
200
197
  else if (isLanded(root, base.ref, name)) {
201
198
  removed.push({ kind: "branch", name, reason: `Landed on ${baseName}` });
@@ -1,5 +1,7 @@
1
1
  /** `git <args>` at `root`, or a stop naming its problem. */
2
2
  export declare function must(root: string, args: string[]): string;
3
+ /** The local branches of the repository at `root`. */
4
+ export declare function branchesOf(root: string): string[];
3
5
  export type Tree = {
4
6
  path: string;
5
7
  head: string;
@@ -15,3 +17,11 @@ export declare function dirtyFiles(path: string): string[] | {
15
17
  };
16
18
  /** The operation paused in the worktree at `path`: its git dir holds the state git resumes it from. */
17
19
  export declare function pausedOperation(path: string): string | undefined;
20
+ /** The dev's way off `branch`, checked out in the main checkout at `root`: null until it is Landed and nothing there could be lost. */
21
+ export declare function switchOff(root: string, base: {
22
+ name: string;
23
+ ref: string;
24
+ }, branch: string): {
25
+ reason: string;
26
+ command: string;
27
+ } | null;
@@ -1,9 +1,11 @@
1
- // What a Sweep reads of a repository's linked worktrees: where each lies, what it has checked out, its lock,
2
- // the files that hold it and the operation paused in it.
1
+ // What a Sweep reads of a repository's branches and worktrees: where each worktree lies, what it has checked
2
+ // out, its lock, the files that hold it and the operation paused in it, and the way off the main checkout's.
3
3
  import { existsSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { exec } from "./exec.js";
6
6
  import { repositoryFreeEnv } from "./git-env.js";
7
+ import { isLanded } from "./landed.js";
8
+ import { shellLine } from "./pr/decide.js";
7
9
  import { CannotRun } from "./pr/read.js";
8
10
  import { asked, git } from "./working-tree.js";
9
11
  /** `git <args>` at `root`, or a stop naming its problem. */
@@ -14,6 +16,12 @@ export function must(root, args) {
14
16
  }
15
17
  return result.out;
16
18
  }
19
+ /** The local branches of the repository at `root`. */
20
+ export function branchesOf(root) {
21
+ return must(root, ["for-each-ref", "--format=%(refname:lstrip=2)", "refs/heads"])
22
+ .split("\n")
23
+ .filter((name) => name !== "");
24
+ }
17
25
  /** Every worktree of the repository, the main checkout first. */
18
26
  export function worktreesOf(root) {
19
27
  const trees = [];
@@ -70,3 +78,15 @@ export function pausedOperation(path) {
70
78
  const dir = asked(path, ["rev-parse", "--absolute-git-dir"]);
71
79
  return dir === undefined ? undefined : OPERATIONS.find(([file]) => existsSync(join(dir, file)))?.[1];
72
80
  }
81
+ /** The dev's way off `branch`, checked out in the main checkout at `root`: null until it is Landed and nothing there could be lost. */
82
+ export function switchOff(root, base, branch) {
83
+ const files = dirtyFiles(root);
84
+ if ("error" in files ||
85
+ files.length > 0 ||
86
+ pausedOperation(root) !== undefined ||
87
+ !isLanded(root, base.ref, branch)) {
88
+ return null;
89
+ }
90
+ const command = `${shellLine(["git", "switch", base.name])} && ${shellLine(["git", "pull", "--rebase"])}`;
91
+ return { reason: `checked out in the main checkout at ${root}, Landed on origin/${base.name}`, command };
92
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dw-sdlc",
3
- "version": "0.18.0",
4
- "description": "The Gate and the agentic config of a repository: the dw-sdlc bin with the Gate, the setup, the doctor, the config check, the Sync and setup of References, and the Ship script that drives a pull request to merged; the resolver of the config's layers, its JSON Schema and the validator that reads it.",
3
+ "version": "0.19.0",
4
+ "description": "The Gate and the agentic config behind the sdlc Claude Code plugin: a bin with the Gate and its Checks, the setup, the doctor, the Sync of References, the Ship script that drives a pull request to merged, the Sweep, and Lead and Worker sessions in herdr; the resolver of a repository's .ai/agentic.config.json layers and its JSON Schema.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",