dw-sdlc 0.18.0 → 0.20.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 +107 -83
- package/config/agentic.config.schema.json +3 -3
- package/dist/agentic-config.d.ts +5 -14
- package/dist/agentic-config.js +12 -6
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/doctor.js +6 -2
- package/dist/commands/setup.d.ts +2 -2
- package/dist/commands/setup.js +4 -4
- package/dist/commands/sweep.js +5 -8
- package/dist/doctor-plugins.js +1 -1
- package/dist/pr/decide.js +6 -3
- package/dist/pr/read.js +36 -5
- package/dist/pr/state.d.ts +7 -1
- package/dist/pr/state.js +6 -1
- package/dist/sweep-trees.d.ts +10 -0
- package/dist/sweep-trees.js +22 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# dw-sdlc
|
|
2
2
|
|
|
3
|
-
The
|
|
4
|
-
works for an agent: its package manager, protected
|
|
5
|
-
Gate and who merges a pull request. The package ships
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
`
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
`
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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,
|
|
113
|
-
| `doctor` | one PASS / WARN / FAIL line per machine check the
|
|
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
|
|
119
|
-
| `recap` | the facts a Recap rests on
|
|
120
|
-
|
|
121
|
-
`
|
|
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
|
-
|
|
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/
|
|
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.
|
|
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 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": {
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"type": "object",
|
|
93
93
|
"properties": {
|
|
94
94
|
"convention": {
|
|
95
|
-
"description": "The subject preset `pattern` falls back to. `ticket-key`: `[KEY-123] type: subject`, the tracker's key in brackets before a type from a fixed list. `conventional`: Conventional Commits
|
|
95
|
+
"description": "The subject preset `pattern` falls back to. `ticket-key`: `[KEY-123] type: subject`, the tracker's key in brackets before a type from a fixed list. `conventional`: Conventional Commits. Never inferred: with no layer setting this or `pattern`, the subject goes unchecked.",
|
|
96
96
|
"enum": ["ticket-key", "conventional"]
|
|
97
97
|
},
|
|
98
98
|
"pattern": {
|
package/dist/agentic-config.d.ts
CHANGED
|
@@ -125,8 +125,8 @@ export type ResolvedConfig = {
|
|
|
125
125
|
};
|
|
126
126
|
guardrails: Guardrails;
|
|
127
127
|
commit: {
|
|
128
|
-
convention
|
|
129
|
-
pattern
|
|
128
|
+
convention?: CommitConvention;
|
|
129
|
+
pattern?: string;
|
|
130
130
|
trailer?: string;
|
|
131
131
|
};
|
|
132
132
|
gate: Gate;
|
|
@@ -136,14 +136,6 @@ export type ResolvedConfig = {
|
|
|
136
136
|
setup: string[];
|
|
137
137
|
};
|
|
138
138
|
};
|
|
139
|
-
/** The layers merged, before `commit.pattern` falls back to the preset its convention names. */
|
|
140
|
-
type Merged = Omit<ResolvedConfig, "commit"> & {
|
|
141
|
-
commit: {
|
|
142
|
-
convention: CommitConvention;
|
|
143
|
-
pattern?: string;
|
|
144
|
-
trailer?: string;
|
|
145
|
-
};
|
|
146
|
-
};
|
|
147
139
|
export type Problem = {
|
|
148
140
|
file: string;
|
|
149
141
|
path: string;
|
|
@@ -210,13 +202,12 @@ export declare function repositoryRoot(dir: string): string;
|
|
|
210
202
|
export declare function mainCheckout(root: string): string | undefined;
|
|
211
203
|
/**
|
|
212
204
|
* The bottom layer: the package manager the lockfile implies, the Gate command the manifest's `check`
|
|
213
|
-
* script implies, plus the defaults every key falls back to.
|
|
214
|
-
* `resolveConfig
|
|
205
|
+
* script implies, plus the defaults every key falls back to. No commit convention: only a file sets
|
|
206
|
+
* one, and `resolveConfig` fills `commit.pattern` from it when no layer sets a pattern.
|
|
215
207
|
*/
|
|
216
|
-
export declare function inferConfig(root: string):
|
|
208
|
+
export declare function inferConfig(root: string): ResolvedConfig;
|
|
217
209
|
/**
|
|
218
210
|
* Local over committed over inferred, for the repository at `root`. A linked worktree reads a layer it
|
|
219
211
|
* lacks from its main checkout, where a repository that keeps `.ai/` out of git has the only copy.
|
|
220
212
|
*/
|
|
221
213
|
export declare function resolveConfig(root: string): Resolution;
|
|
222
|
-
export {};
|
package/dist/agentic-config.js
CHANGED
|
@@ -207,8 +207,8 @@ function inferGate(root) {
|
|
|
207
207
|
}
|
|
208
208
|
/**
|
|
209
209
|
* The bottom layer: the package manager the lockfile implies, the Gate command the manifest's `check`
|
|
210
|
-
* script implies, plus the defaults every key falls back to.
|
|
211
|
-
* `resolveConfig
|
|
210
|
+
* script implies, plus the defaults every key falls back to. No commit convention: only a file sets
|
|
211
|
+
* one, and `resolveConfig` fills `commit.pattern` from it when no layer sets a pattern.
|
|
212
212
|
*/
|
|
213
213
|
export function inferConfig(root) {
|
|
214
214
|
return {
|
|
@@ -216,7 +216,7 @@ export function inferConfig(root) {
|
|
|
216
216
|
baseBranch: "auto",
|
|
217
217
|
packageManagers: inferPackageManagers(root),
|
|
218
218
|
guardrails: cloneGuardrails(DEFAULT_GUARDRAILS),
|
|
219
|
-
commit: {
|
|
219
|
+
commit: {},
|
|
220
220
|
gate: inferGate(root),
|
|
221
221
|
pr: { ...DEFAULT_PR },
|
|
222
222
|
worker: { ...DEFAULT_WORKER },
|
|
@@ -307,10 +307,16 @@ function prLoosenings(base, local) {
|
|
|
307
307
|
return [{ file: LOCAL_FILE, path: `pr.${key}`, message }];
|
|
308
308
|
});
|
|
309
309
|
}
|
|
310
|
-
/**
|
|
310
|
+
/**
|
|
311
|
+
* `commit.pattern` from the convention's preset when no layer set one; a set pattern wins. Neither
|
|
312
|
+
* set, no pattern: the subject goes unchecked.
|
|
313
|
+
*/
|
|
311
314
|
function finalize(merged) {
|
|
312
|
-
const
|
|
313
|
-
|
|
315
|
+
const { convention, pattern } = merged.commit;
|
|
316
|
+
if (pattern !== undefined || convention === undefined) {
|
|
317
|
+
return merged;
|
|
318
|
+
}
|
|
319
|
+
return { ...merged, commit: { ...merged.commit, pattern: COMMIT_PATTERNS[convention] } };
|
|
314
320
|
}
|
|
315
321
|
/**
|
|
316
322
|
* Local over committed over inferred, for the repository at `root`. A linked worktree reads a layer it
|
package/dist/commands/doctor.js
CHANGED
|
@@ -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 {
|
|
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) {
|
|
@@ -219,7 +223,7 @@ function checkConfig(config, root) {
|
|
|
219
223
|
return {
|
|
220
224
|
name: "config",
|
|
221
225
|
level: "PASS",
|
|
222
|
-
detail: `commit ${config.config.commit.convention}${borrowed.join("")}`
|
|
226
|
+
detail: `commit ${config.config.commit.convention ?? "unchecked"}${borrowed.join("")}`
|
|
223
227
|
};
|
|
224
228
|
}
|
|
225
229
|
export function diagnose(inputs) {
|
package/dist/commands/setup.d.ts
CHANGED
|
@@ -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
|
|
31
|
-
*
|
|
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[];
|
package/dist/commands/setup.js
CHANGED
|
@@ -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
|
|
73
|
-
*
|
|
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"));
|
|
@@ -168,8 +168,8 @@ function localConfig(root, claudeDir, convention, guardrails, worker) {
|
|
|
168
168
|
if (worker !== undefined) {
|
|
169
169
|
config.worker = worker;
|
|
170
170
|
}
|
|
171
|
-
// Only one asked for, by `--convention` or the file's pin:
|
|
172
|
-
//
|
|
171
|
+
// Only one asked for, by `--convention` or the file's pin: pinning one here would override a
|
|
172
|
+
// convention the repository commits later.
|
|
173
173
|
if (convention !== undefined) {
|
|
174
174
|
config.commit = { convention };
|
|
175
175
|
}
|
package/dist/commands/sweep.js
CHANGED
|
@@ -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 {
|
|
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}` });
|
package/dist/doctor-plugins.js
CHANGED
|
@@ -146,7 +146,7 @@ export function checkSessions(probe, claude) {
|
|
|
146
146
|
if (entry === undefined) {
|
|
147
147
|
const detail = list === undefined
|
|
148
148
|
? "claude plugin list did not answer"
|
|
149
|
-
: `no ${SESSIONS_ID}, so no /sdlc-recap, /sdlc-
|
|
149
|
+
: `no ${SESSIONS_ID}, so no /sdlc-recap, /sdlc-kickoff or /sdlc-delegate. claude plugin install ${SESSIONS_ID} --scope user`;
|
|
150
150
|
return { name, level: "INFO", detail };
|
|
151
151
|
}
|
|
152
152
|
const version = typeof entry.version === "string" ? entry.version : "?";
|
package/dist/pr/decide.js
CHANGED
|
@@ -232,8 +232,10 @@ const ROWS = [
|
|
|
232
232
|
return { ...head, action: "mark-reviewed", actor: "script", details: after };
|
|
233
233
|
}
|
|
234
234
|
const { depth, source } = depthOf(pr, policy);
|
|
235
|
-
// The dev's own depth buys one Review past the limit
|
|
236
|
-
|
|
235
|
+
// The dev's own depth buys one Review past the limit, and only one: the flag rides on every
|
|
236
|
+
// later call. A Review under off needs none.
|
|
237
|
+
const limit = source === "flag" ? REVIEW_LIMIT + 1 : REVIEW_LIMIT;
|
|
238
|
+
if (depth !== "off" && pr.marks.count >= limit) {
|
|
237
239
|
return reviewLimit(pr);
|
|
238
240
|
}
|
|
239
241
|
const byScript = depth === "off";
|
|
@@ -246,7 +248,8 @@ const ROWS = [
|
|
|
246
248
|
source,
|
|
247
249
|
// The agent may raise a routed depth to full, so auto names the command at any depth.
|
|
248
250
|
...(depth === "full" || source === "auto" ? { reviewCommand: policy.reviewCommand } : {}),
|
|
249
|
-
...after
|
|
251
|
+
...after,
|
|
252
|
+
...(pr.scope === null ? {} : { paths: pr.scope })
|
|
250
253
|
}
|
|
251
254
|
};
|
|
252
255
|
}
|
package/dist/pr/read.js
CHANGED
|
@@ -205,16 +205,47 @@ function withCarried(pr) {
|
|
|
205
205
|
return { ...pr, carried: sameChanges(ownChanges(pr, last.commit), ownChanges(pr, pr.head)) };
|
|
206
206
|
}
|
|
207
207
|
/**
|
|
208
|
-
*
|
|
209
|
-
*
|
|
208
|
+
* The files whose own change differs between `from` and the head of `pr`, a rename under both names:
|
|
209
|
+
* what a fix touched, without the files a merge of the base brought in. Null when a compare cannot show
|
|
210
|
+
* the own changes whole.
|
|
211
|
+
*/
|
|
212
|
+
function ownScope(pr, from) {
|
|
213
|
+
const before = ownChanges(pr, from);
|
|
214
|
+
const after = ownChanges(pr, pr.head);
|
|
215
|
+
if (before === null || after === null) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
const moved = [
|
|
219
|
+
...after.filter((change) => !before.includes(change)),
|
|
220
|
+
...before.filter((change) => !after.includes(change))
|
|
221
|
+
];
|
|
222
|
+
const names = moved.flatMap((change) => {
|
|
223
|
+
const [path, , previous] = JSON.parse(change);
|
|
224
|
+
return previous === null ? [path] : [path, previous];
|
|
225
|
+
});
|
|
226
|
+
return [...new Set(names)].toSorted();
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* `pr` and, when a Review awaits a head that carries none, its `scope` after a merge of the base, and
|
|
230
|
+
* when `sized`, the files the Review would read: the compare from the last commit a Review read, else
|
|
231
|
+
* from the base, to the head, kept to the scope when there is one.
|
|
210
232
|
*/
|
|
211
233
|
function withChanged(pr, sized) {
|
|
212
|
-
if (
|
|
213
|
-
return { ...pr, changed: null };
|
|
234
|
+
if (pr.carried || pr.state !== "OPEN" || !awaitsReview(pr)) {
|
|
235
|
+
return { ...pr, changed: null, scope: null };
|
|
236
|
+
}
|
|
237
|
+
const scope = pr.lastRead?.merges === true ? ownScope(pr, pr.lastRead.commit) : null;
|
|
238
|
+
if (!sized) {
|
|
239
|
+
return { ...pr, changed: null, scope };
|
|
214
240
|
}
|
|
215
241
|
const from = pr.lastRead?.commit ?? pr.baseOid;
|
|
216
242
|
const reply = compared(pr, from, pr.head, `the compare of ${from.slice(0, 7)}...${pr.head.slice(0, 7)}`);
|
|
217
|
-
|
|
243
|
+
const changed = parseOrCannotRun(() => changedOf(reply));
|
|
244
|
+
if (scope === null || changed === "unsized") {
|
|
245
|
+
return { ...pr, changed, scope };
|
|
246
|
+
}
|
|
247
|
+
const within = changed.filter(({ path, previous }) => scope.includes(path) || (previous !== null && scope.includes(previous)));
|
|
248
|
+
return { ...pr, changed: within, scope };
|
|
218
249
|
}
|
|
219
250
|
/** The state of pull request `number` of `repo`, from one GraphQL request and the compares a Review needs. */
|
|
220
251
|
function query({ repo, number }, sized) {
|
package/dist/pr/state.d.ts
CHANGED
|
@@ -88,6 +88,11 @@ export type PrState = {
|
|
|
88
88
|
* auto` to route on: `unsized` when the compare cannot list them all, null when not read.
|
|
89
89
|
*/
|
|
90
90
|
changed: ChangedFile[] | "unsized" | null;
|
|
91
|
+
/**
|
|
92
|
+
* The files whose own change moved since `lastRead`, when a merge of the base came after it: the files
|
|
93
|
+
* a fix touched, the base's own left out. Null with no such merge, or when a compare cannot show them.
|
|
94
|
+
*/
|
|
95
|
+
scope: string[] | null;
|
|
91
96
|
};
|
|
92
97
|
/**
|
|
93
98
|
* A file a Review would read: its name, its name before a rename, and the lines it adds and removes,
|
|
@@ -106,6 +111,7 @@ export type LastRead = {
|
|
|
106
111
|
commit: string;
|
|
107
112
|
clean: boolean;
|
|
108
113
|
mergesOnly: boolean;
|
|
114
|
+
merges: boolean;
|
|
109
115
|
};
|
|
110
116
|
/**
|
|
111
117
|
* The Reviews that left a Finding open since the last review success, a success under off and the dev's
|
|
@@ -144,5 +150,5 @@ export declare function parseView(reply: string): {
|
|
|
144
150
|
/** The default branch of the repository whose `gh repo view --json defaultBranchRef` printed `reply`. */
|
|
145
151
|
export declare function parseDefaultBranch(reply: string): string;
|
|
146
152
|
/** The state of the pull request in `reply`, the text `gh api graphql` printed for QUERY on `repo`. */
|
|
147
|
-
export declare function parsePr(reply: string, repo: string): Omit<PrState, "carried" | "changed">;
|
|
153
|
+
export declare function parsePr(reply: string, repo: string): Omit<PrState, "carried" | "changed" | "scope">;
|
|
148
154
|
export {};
|
package/dist/pr/state.js
CHANGED
|
@@ -253,7 +253,12 @@ function lastReadOf(history, head) {
|
|
|
253
253
|
if (last === undefined) {
|
|
254
254
|
return null;
|
|
255
255
|
}
|
|
256
|
-
return {
|
|
256
|
+
return {
|
|
257
|
+
commit: last.oid,
|
|
258
|
+
clean: last.read === "clean",
|
|
259
|
+
mergesOnly: mergesOnlyAfter(index),
|
|
260
|
+
merges: commits.slice(index + 1).some(({ merge }) => merge)
|
|
261
|
+
};
|
|
257
262
|
}
|
|
258
263
|
/**
|
|
259
264
|
* Whether a non-merge commit follows `commit` in the history, oldest first, as `lastReadOf` judges a read
|
package/dist/sweep-trees.d.ts
CHANGED
|
@@ -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;
|
package/dist/sweep-trees.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
// What a Sweep reads of a repository's
|
|
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.
|
|
4
|
-
"description": "The Gate and the agentic config
|
|
3
|
+
"version": "0.20.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",
|