dw-sdlc 0.19.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.
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
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`, `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.",
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; inferred when no layer sets either.",
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": {
@@ -125,8 +125,8 @@ export type ResolvedConfig = {
125
125
  };
126
126
  guardrails: Guardrails;
127
127
  commit: {
128
- convention: CommitConvention;
129
- pattern: string;
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. `commit.pattern` is left to
214
- * `resolveConfig`, which fills it from the convention when no layer sets one.
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): Merged;
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 {};
@@ -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. `commit.pattern` is left to
211
- * `resolveConfig`, which fills it from the convention when no layer sets one.
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: { convention: "conventional" },
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
- /** `commit.pattern` from the convention's preset when no layer set one; a set pattern wins. */
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 pattern = merged.commit.pattern ?? COMMIT_PATTERNS[merged.commit.convention];
313
- return { ...merged, commit: { ...merged.commit, pattern } };
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
@@ -19,7 +19,7 @@ export type Probe = {
19
19
  export type RepositoryConfig = {
20
20
  config: {
21
21
  commit: {
22
- convention: string;
22
+ convention?: string;
23
23
  };
24
24
  };
25
25
  problems: string[];
@@ -223,7 +223,7 @@ function checkConfig(config, root) {
223
223
  return {
224
224
  name: "config",
225
225
  level: "PASS",
226
- detail: `commit ${config.config.commit.convention}${borrowed.join("")}`
226
+ detail: `commit ${config.config.commit.convention ?? "unchecked"}${borrowed.join("")}`
227
227
  };
228
228
  }
229
229
  export function diagnose(inputs) {
@@ -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: inference always gives `conventional`, and
172
- // pinning it here would override a convention the repository commits later.
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
  }
@@ -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-brief or /sdlc-delegate. claude plugin install ${SESSIONS_ID} --scope user`;
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; a Review under off needs none.
236
- if (depth !== "off" && source !== "flag" && pr.marks.count >= REVIEW_LIMIT) {
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
- * `pr` and, when `sized` and a Review awaits a head that carries none, the files the Review would read:
209
- * the compare from the last commit a Review read, else from the base, to the head.
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 (!sized || pr.carried || pr.state !== "OPEN" || !awaitsReview(pr)) {
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
- return { ...pr, changed: parseOrCannotRun(() => changedOf(reply)) };
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) {
@@ -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 { commit: last.oid, clean: last.read === "clean", mergesOnly: mergesOnlyAfter(index) };
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dw-sdlc",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
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": {