dw-sdlc 0.20.0 → 0.21.1
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 +13 -13
- package/config/agentic.config.schema.json +1 -1
- package/dist/agentic-config.d.ts +16 -0
- package/dist/agentic-config.js +28 -23
- package/dist/cannot-run.d.ts +27 -0
- package/dist/cannot-run.js +48 -0
- package/dist/catalogue.js +2 -1
- package/dist/checks/agents-map.js +7 -8
- package/dist/checks/check.d.ts +10 -8
- package/dist/checks/check.js +9 -7
- package/dist/checks/decision-records.js +3 -2
- package/dist/checks/file-size-ratchet.js +13 -17
- package/dist/checks/index.d.ts +6 -5
- package/dist/checks/index.js +10 -9
- package/dist/checks/markdown-refs.js +58 -6
- package/dist/checks/primitives.js +5 -5
- package/dist/checks/routed-docs.js +1 -3
- package/dist/checks/skill-sections.js +12 -20
- package/dist/checks/tree.js +7 -7
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +26 -5
- package/dist/commands/check.js +27 -42
- package/dist/commands/config.js +22 -27
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +11 -18
- package/dist/commands/gate.js +35 -40
- package/dist/commands/lead-start.js +28 -42
- package/dist/commands/pr-reviewed.js +41 -59
- package/dist/commands/pr-ship.js +72 -87
- package/dist/commands/primitives.js +38 -40
- package/dist/commands/recap.js +23 -74
- package/dist/commands/references-setup.js +28 -76
- package/dist/commands/references-sync.js +29 -53
- package/dist/commands/setup.d.ts +2 -7
- package/dist/commands/setup.js +38 -90
- package/dist/commands/sweep.js +46 -157
- package/dist/commands/worker-ls.js +15 -31
- package/dist/commands/worker-report.js +36 -52
- package/dist/commands/worker-start.js +43 -57
- package/dist/drift.js +2 -1
- package/dist/exec.d.ts +20 -0
- package/dist/exec.js +37 -4
- package/dist/gate-cache.js +2 -1
- package/dist/git.d.ts +32 -0
- package/dist/git.js +60 -0
- package/dist/json-schema.js +1 -3
- package/dist/landed.js +11 -22
- package/dist/paused-operation.d.ts +2 -0
- package/dist/paused-operation.js +17 -0
- package/dist/pins.js +2 -1
- package/dist/pr/decide.d.ts +4 -16
- package/dist/pr/decide.js +48 -83
- package/dist/pr/delta.d.ts +3 -4
- package/dist/pr/delta.js +11 -20
- package/dist/pr/read.d.ts +27 -30
- package/dist/pr/read.js +96 -158
- package/dist/pr/reply.d.ts +34 -0
- package/dist/pr/reply.js +112 -0
- package/dist/pr/route.d.ts +11 -3
- package/dist/pr/route.js +5 -5
- package/dist/pr/standing.d.ts +138 -0
- package/dist/pr/standing.js +181 -0
- package/dist/pr/state.d.ts +14 -69
- package/dist/pr/state.js +75 -148
- package/dist/pr/upstream.d.ts +1 -6
- package/dist/pr/upstream.js +2 -2
- package/dist/primitives-diff.js +1 -1
- package/dist/recap-next.js +34 -47
- package/dist/recap-primitives.js +1 -1
- package/dist/references-key.d.ts +0 -1
- package/dist/references-key.js +3 -4
- package/dist/run-command.d.ts +4 -1
- package/dist/run-command.js +32 -11
- package/dist/setup-gate.d.ts +1 -7
- package/dist/setup-rules.js +2 -6
- package/dist/setup-writer.d.ts +32 -0
- package/dist/setup-writer.js +60 -0
- package/dist/shell-line.d.ts +2 -0
- package/dist/shell-line.js +5 -0
- package/dist/sweep-removal.d.ts +31 -0
- package/dist/sweep-removal.js +133 -0
- package/dist/sweep-terminals.d.ts +1 -1
- package/dist/sweep-terminals.js +21 -57
- package/dist/sweep-trees.d.ts +19 -3
- package/dist/sweep-trees.js +67 -31
- package/dist/worker/herdr.d.ts +67 -23
- package/dist/worker/herdr.js +126 -77
- package/dist/worker/launch.js +32 -45
- package/dist/worker/origin.d.ts +1 -1
- package/dist/worker/origin.js +9 -11
- package/dist/worker/tabs.js +11 -7
- package/dist/working-tree.d.ts +0 -8
- package/dist/working-tree.js +6 -22
- package/dist/worktree-lock.js +1 -1
- package/package.json +1 -1
- package/dist/git-env.d.ts +0 -2
- package/dist/git-env.js +0 -10
package/README.md
CHANGED
|
@@ -65,19 +65,19 @@ line; a `gate.checks.run` entry takes a `hint` beside `name` and `command`.
|
|
|
65
65
|
Under `gate.checks`, a Check is `true` for its defaults, an object of its options, or `false`. The
|
|
66
66
|
schema's description of each one names its options and defaults.
|
|
67
67
|
|
|
68
|
-
| Check | fails
|
|
69
|
-
| -------------------- |
|
|
70
|
-
| `markdown-refs` | a relative link or
|
|
71
|
-
| `temporal-language` | a changelog phrase in a durable document, outside code
|
|
72
|
-
| `comment-budget` | a comment block over 3 lines of prose in JS, TS or shell
|
|
73
|
-
| `disable-directives` | a linter or type-checker disable comment with no reason
|
|
74
|
-
| `agents-map` | an `AGENTS.md` over budget, missing the Gate command or pointing at nothing
|
|
75
|
-
| `primitives` | a catalogue entry out of step with the tree, or a file no entry owns
|
|
76
|
-
| `routed-docs` | a document under `docs/` no Task Router row or Primitive leads to
|
|
77
|
-
| `markdown-budget` | a markdown file over the byte budget of its glob
|
|
78
|
-
| `decision-records` | a decision record misnamed, misnumbered or without a valid status
|
|
79
|
-
| `file-size-ratchet` | a code file over its line limit that the baseline does not allow
|
|
80
|
-
| `skill-sections` | a README section a skill copies that differs from its source
|
|
68
|
+
| Check | fails | reads first |
|
|
69
|
+
| -------------------- | ------------------------------------------------------------------------------ | ------------------------- |
|
|
70
|
+
| `markdown-refs` | a relative link, image or repository URL whose path or 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
81
|
|
|
82
82
|
Each Check's options and defaults:
|
|
83
83
|
[its description in the schema](https://github.com/dominikwozniak/dw-sdlc/blob/main/packages/dw-sdlc/config/agentic.config.schema.json).
|
|
@@ -264,7 +264,7 @@
|
|
|
264
264
|
"additionalProperties": false
|
|
265
265
|
},
|
|
266
266
|
"markdown-refs": {
|
|
267
|
-
"description": "Every relative link and image in the markdown files resolves: its path to a file or directory git tracks or would track, its heading anchor to a heading of the markdown file it names, and `#anchor` alone to a heading of its own file. A link with a scheme, such as `https:`, and a link inside code are not judged.",
|
|
267
|
+
"description": "Every relative link and image in the markdown files resolves: its path to a file or directory git tracks or would track, its heading anchor to a heading of the markdown file it names, and `#anchor` alone to a heading of its own file. A URL into the GitHub repository `origin` names, at its default branch (the one `origin/HEAD` names, else `main`), as a `blob`, `tree` or raw file URL, resolves the same way, inside code too. Any other link with a scheme, such as `https:`, and a relative link inside code are not judged.",
|
|
268
268
|
"type": ["boolean", "object"],
|
|
269
269
|
"properties": {
|
|
270
270
|
"files": {
|
package/dist/agentic-config.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type CommitConvention = (typeof COMMIT_CONVENTIONS)[number];
|
|
|
8
8
|
export type NodePackageManager = "pnpm" | "npm" | "yarn" | "bun";
|
|
9
9
|
export type ForcePush = "deny" | "ask" | "lease";
|
|
10
10
|
export type CommitOnProtected = "deny" | "allow";
|
|
11
|
+
export type FilteredOutput = "deny" | "allow";
|
|
11
12
|
export type EnvPolicy = "deny" | "git";
|
|
12
13
|
/** Which env files may be read: `deny` list, then `allow` list, then `policy` for the rest. */
|
|
13
14
|
export type EnvFiles = {
|
|
@@ -47,6 +48,7 @@ export type WorkerConfig = {
|
|
|
47
48
|
export type Guardrails = {
|
|
48
49
|
forcePush: ForcePush;
|
|
49
50
|
commitOnProtected: CommitOnProtected;
|
|
51
|
+
filteredOutput: FilteredOutput;
|
|
50
52
|
envFiles: EnvFiles;
|
|
51
53
|
protectedBranches: string[];
|
|
52
54
|
};
|
|
@@ -159,6 +161,7 @@ export type Resolution = {
|
|
|
159
161
|
export declare const STRICTNESS: {
|
|
160
162
|
readonly forcePush: readonly ["deny", "ask", "lease"];
|
|
161
163
|
readonly commitOnProtected: readonly ["deny", "allow"];
|
|
164
|
+
readonly filteredOutput: readonly ["deny", "allow"];
|
|
162
165
|
readonly envFiles: readonly ["deny", "git"];
|
|
163
166
|
readonly merge: readonly ["ask", "auto"];
|
|
164
167
|
readonly review: readonly ["full", "auto", "light", "off"];
|
|
@@ -206,8 +209,21 @@ export declare function mainCheckout(root: string): string | undefined;
|
|
|
206
209
|
* one, and `resolveConfig` fills `commit.pattern` from it when no layer sets a pattern.
|
|
207
210
|
*/
|
|
208
211
|
export declare function inferConfig(root: string): ResolvedConfig;
|
|
212
|
+
type CommitRules = {
|
|
213
|
+
commit: {
|
|
214
|
+
convention?: string;
|
|
215
|
+
pattern?: string;
|
|
216
|
+
trailer?: string;
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
/**
|
|
220
|
+
* What a report says is in force while a layer is invalid: the strictest guardrails, and no commit
|
|
221
|
+
* rule where no valid layer sets a convention, pattern or trailer, since the inferred layer names none.
|
|
222
|
+
*/
|
|
223
|
+
export declare function invalidLayerEffect({ commit }: CommitRules): string;
|
|
209
224
|
/**
|
|
210
225
|
* Local over committed over inferred, for the repository at `root`. A linked worktree reads a layer it
|
|
211
226
|
* lacks from its main checkout, where a repository that keeps `.ai/` out of git has the only copy.
|
|
212
227
|
*/
|
|
213
228
|
export declare function resolveConfig(root: string): Resolution;
|
|
229
|
+
export {};
|
package/dist/agentic-config.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// `hooks/config.jq` is the same merge for the guards; this file is the reference, and the tests run both.
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
|
-
import {
|
|
6
|
+
import { messageOf } from "./cannot-run.js";
|
|
7
|
+
import { asked, gitDirs } from "./git.js";
|
|
7
8
|
import { validate } from "./json-schema.js";
|
|
8
9
|
export const COMMITTED_FILE = ".ai/agentic.config.json";
|
|
9
10
|
export const LOCAL_FILE = ".ai/agentic.config.local.json";
|
|
@@ -27,6 +28,7 @@ export function stepHint(step) {
|
|
|
27
28
|
export const STRICTNESS = {
|
|
28
29
|
forcePush: ["deny", "ask", "lease"],
|
|
29
30
|
commitOnProtected: ["deny", "allow"],
|
|
31
|
+
filteredOutput: ["deny", "allow"],
|
|
30
32
|
envFiles: ["deny", "git"],
|
|
31
33
|
merge: ["ask", "auto"],
|
|
32
34
|
review: ["full", "auto", "light", "off"]
|
|
@@ -35,6 +37,7 @@ export const STRICTNESS = {
|
|
|
35
37
|
export const STRICTEST_GUARDRAILS = {
|
|
36
38
|
forcePush: "deny",
|
|
37
39
|
commitOnProtected: "deny",
|
|
40
|
+
filteredOutput: "deny",
|
|
38
41
|
envFiles: { policy: "deny", allow: [], deny: [] },
|
|
39
42
|
protectedBranches: ["master", "main", "production"]
|
|
40
43
|
};
|
|
@@ -53,6 +56,7 @@ const DEFAULT_WORKER = { command: "claude", model: "sonnet", effort: "high" };
|
|
|
53
56
|
export const DEFAULT_GUARDRAILS = {
|
|
54
57
|
forcePush: "lease",
|
|
55
58
|
commitOnProtected: "allow",
|
|
59
|
+
filteredOutput: "deny",
|
|
56
60
|
envFiles: { policy: "git", allow: ["**/.env.example", "**/.env.sample", "**/.env.template"], deny: [] },
|
|
57
61
|
protectedBranches: ["master", "main", "production"]
|
|
58
62
|
};
|
|
@@ -60,6 +64,7 @@ function cloneGuardrails(source) {
|
|
|
60
64
|
return {
|
|
61
65
|
forcePush: source.forcePush,
|
|
62
66
|
commitOnProtected: source.commitOnProtected,
|
|
67
|
+
filteredOutput: source.filteredOutput,
|
|
63
68
|
envFiles: { policy: source.envFiles.policy, allow: [...source.envFiles.allow], deny: [...source.envFiles.deny] },
|
|
64
69
|
protectedBranches: [...source.protectedBranches]
|
|
65
70
|
};
|
|
@@ -99,9 +104,6 @@ const NODE_LOCKFILES = [
|
|
|
99
104
|
["bun.lock", "bun"],
|
|
100
105
|
["bun.lockb", "bun"]
|
|
101
106
|
];
|
|
102
|
-
function messageOf(error) {
|
|
103
|
-
return error instanceof Error ? error.message : String(error);
|
|
104
|
-
}
|
|
105
107
|
function show(value) {
|
|
106
108
|
return JSON.stringify(value);
|
|
107
109
|
}
|
|
@@ -151,8 +153,7 @@ function readLayer(checkouts, file) {
|
|
|
151
153
|
}
|
|
152
154
|
/** The top level of the repository holding `dir`, or undefined outside one. */
|
|
153
155
|
export function findRepository(dir) {
|
|
154
|
-
|
|
155
|
-
return toplevel.status === 0 ? toplevel.stdout.trim() : undefined;
|
|
156
|
+
return asked(dir, ["rev-parse", "--show-toplevel"]);
|
|
156
157
|
}
|
|
157
158
|
/** The top level of the repository holding `dir`, or `dir` itself outside one. */
|
|
158
159
|
export function repositoryRoot(dir) {
|
|
@@ -163,13 +164,8 @@ export function repositoryRoot(dir) {
|
|
|
163
164
|
* repository: a linked worktree's git dir differs from the common one, which sits in the main checkout.
|
|
164
165
|
*/
|
|
165
166
|
export function mainCheckout(root) {
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
if (result.status !== 0) {
|
|
169
|
-
return undefined;
|
|
170
|
-
}
|
|
171
|
-
const [gitDir, commonDir] = result.stdout.trim().split("\n");
|
|
172
|
-
return gitDir === undefined || commonDir === undefined || gitDir === commonDir ? undefined : dirname(commonDir);
|
|
167
|
+
const dirs = gitDirs(root);
|
|
168
|
+
return dirs === undefined || dirs.gitDir === dirs.commonDir ? undefined : dirname(dirs.commonDir);
|
|
173
169
|
}
|
|
174
170
|
function inferPackageManagers(root) {
|
|
175
171
|
const managers = {};
|
|
@@ -245,24 +241,24 @@ function rank(key, value) {
|
|
|
245
241
|
return order.indexOf(value);
|
|
246
242
|
}
|
|
247
243
|
const LOOSENS = "the local file may tighten guardrails, never loosen them";
|
|
244
|
+
/** The guardrails that hold one value, each ranked under its own name in `STRICTNESS`. */
|
|
245
|
+
const RANKED_GUARDRAILS = ["forcePush", "commitOnProtected", "filteredOutput"];
|
|
248
246
|
/**
|
|
249
247
|
* Every guardrail the local layer sets less strictly than what it would override: a lower-ranked
|
|
250
|
-
* `forcePush`, `commitOnProtected` or `envFiles.policy`, an added `envFiles.allow`
|
|
251
|
-
* `envFiles.deny` entry, a dropped protected branch.
|
|
248
|
+
* `forcePush`, `commitOnProtected`, `filteredOutput` or `envFiles.policy`, an added `envFiles.allow`
|
|
249
|
+
* entry, a dropped `envFiles.deny` entry, a dropped protected branch.
|
|
252
250
|
*/
|
|
253
251
|
function loosenings(base, local) {
|
|
254
252
|
if (local === undefined) {
|
|
255
253
|
return [];
|
|
256
254
|
}
|
|
257
255
|
const problems = [];
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const message = `${show(local.commitOnProtected)} loosens ${show(base.commitOnProtected)}; ${LOOSENS}`;
|
|
265
|
-
problems.push({ file: LOCAL_FILE, path: "guardrails.commitOnProtected", message });
|
|
256
|
+
for (const key of RANKED_GUARDRAILS) {
|
|
257
|
+
const value = local[key];
|
|
258
|
+
if (value !== undefined && rank(key, value) > rank(key, base[key])) {
|
|
259
|
+
const message = `${show(value)} loosens ${show(base[key])}; ${LOOSENS}`;
|
|
260
|
+
problems.push({ file: LOCAL_FILE, path: `guardrails.${key}`, message });
|
|
261
|
+
}
|
|
266
262
|
}
|
|
267
263
|
const policy = local.envFiles?.policy;
|
|
268
264
|
if (policy !== undefined && rank("envFiles", policy) > rank("envFiles", base.envFiles.policy)) {
|
|
@@ -318,6 +314,15 @@ function finalize(merged) {
|
|
|
318
314
|
}
|
|
319
315
|
return { ...merged, commit: { ...merged.commit, pattern: COMMIT_PATTERNS[convention] } };
|
|
320
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* What a report says is in force while a layer is invalid: the strictest guardrails, and no commit
|
|
319
|
+
* rule where no valid layer sets a convention, pattern or trailer, since the inferred layer names none.
|
|
320
|
+
*/
|
|
321
|
+
export function invalidLayerEffect({ commit }) {
|
|
322
|
+
const strictest = "the guards run on the strictest defaults";
|
|
323
|
+
const unchecked = commit.convention === undefined && commit.pattern === undefined && commit.trailer === undefined;
|
|
324
|
+
return unchecked ? `${strictest} and the commit rules are off` : strictest;
|
|
325
|
+
}
|
|
321
326
|
/**
|
|
322
327
|
* Local over committed over inferred, for the repository at `root`. A linked worktree reads a layer it
|
|
323
328
|
* lacks from its main checkout, where a repository that keeps `.ai/` out of git has the only copy.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { parseArgs, type ParseArgsConfig } from "node:util";
|
|
2
|
+
/** Where the problem sits, and what the bin prints below its line. */
|
|
3
|
+
export type Detail = {
|
|
4
|
+
/** The file, and its line, of an input a Check reads. */
|
|
5
|
+
file?: string;
|
|
6
|
+
line?: number;
|
|
7
|
+
/** Lines the bin prints below the problem's, as a command's usage. */
|
|
8
|
+
after?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare class CannotRun extends Error {
|
|
11
|
+
readonly remedy: string | undefined;
|
|
12
|
+
readonly detail: Detail;
|
|
13
|
+
constructor(message: string, remedy?: string, detail?: Detail);
|
|
14
|
+
/** The same problem, with `remedy` where it names none. */
|
|
15
|
+
orRemedy(remedy: string): CannotRun;
|
|
16
|
+
}
|
|
17
|
+
/** What an error says, whatever was thrown. */
|
|
18
|
+
export declare function messageOf(error: unknown): string;
|
|
19
|
+
/** A command line the command cannot read: the problem, then its usage after an empty line. */
|
|
20
|
+
export declare function usageError(problem: string, usage: string): CannotRun;
|
|
21
|
+
/** `parseArgs` over a command line; one it cannot read stops the command with its usage. */
|
|
22
|
+
export declare function readArgs<T extends ParseArgsConfig>(config: T, usage: string): ReturnType<typeof parseArgs<T>>;
|
|
23
|
+
/**
|
|
24
|
+
* The directory `--root` names, or the current one without it. A `--root` that names no directory
|
|
25
|
+
* stops the command, which would otherwise read the empty repository a typo leaves.
|
|
26
|
+
*/
|
|
27
|
+
export declare function rootDir(root: string | undefined): string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// What stops a command before it reaches a verdict: exit 2, one problem and, where there is one, its
|
|
2
|
+
// remedy. The bin alone prints it, as `dw-sdlc <command>: <problem> — <remedy>`; the Gate reports a
|
|
3
|
+
// Check's as one of its failures. A command line the command cannot read carries its usage below.
|
|
4
|
+
import { statSync } from "node:fs";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
export class CannotRun extends Error {
|
|
7
|
+
remedy;
|
|
8
|
+
detail;
|
|
9
|
+
constructor(message, remedy, detail = {}) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.remedy = remedy;
|
|
12
|
+
this.detail = detail;
|
|
13
|
+
}
|
|
14
|
+
/** The same problem, with `remedy` where it names none. */
|
|
15
|
+
orRemedy(remedy) {
|
|
16
|
+
return this.remedy === undefined ? new CannotRun(this.message, remedy, this.detail) : this;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** What an error says, whatever was thrown. */
|
|
20
|
+
export function messageOf(error) {
|
|
21
|
+
return error instanceof Error ? error.message : String(error);
|
|
22
|
+
}
|
|
23
|
+
/** A command line the command cannot read: the problem, then its usage after an empty line. */
|
|
24
|
+
export function usageError(problem, usage) {
|
|
25
|
+
return new CannotRun(problem, undefined, { after: `\n${usage}` });
|
|
26
|
+
}
|
|
27
|
+
/** `parseArgs` over a command line; one it cannot read stops the command with its usage. */
|
|
28
|
+
export function readArgs(config, usage) {
|
|
29
|
+
try {
|
|
30
|
+
return parseArgs(config);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
throw usageError(messageOf(error), usage);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The directory `--root` names, or the current one without it. A `--root` that names no directory
|
|
38
|
+
* stops the command, which would otherwise read the empty repository a typo leaves.
|
|
39
|
+
*/
|
|
40
|
+
export function rootDir(root) {
|
|
41
|
+
if (root === undefined) {
|
|
42
|
+
return process.cwd();
|
|
43
|
+
}
|
|
44
|
+
if (statSync(root, { throwIfNoEntry: false })?.isDirectory() !== true) {
|
|
45
|
+
throw new CannotRun(`--root ${root} is not a directory`, "pass --root a directory inside the repository");
|
|
46
|
+
}
|
|
47
|
+
return root;
|
|
48
|
+
}
|
package/dist/catalogue.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// says and `dw-sdlc primitives --paths` answers from it.
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
+
import { messageOf } from "./cannot-run.js";
|
|
6
7
|
import { validate } from "./json-schema.js";
|
|
7
8
|
/** Where a repository keeps its catalogue unless its config says otherwise. */
|
|
8
9
|
export const CATALOGUE_FILE = ".ai/primitives.json";
|
|
@@ -41,7 +42,7 @@ export function parseCatalogue(text) {
|
|
|
41
42
|
parsed = JSON.parse(text);
|
|
42
43
|
}
|
|
43
44
|
catch (error) {
|
|
44
|
-
return { state: "invalid", problems: [`not JSON: ${
|
|
45
|
+
return { state: "invalid", problems: [`not JSON: ${messageOf(error)}`] };
|
|
45
46
|
}
|
|
46
47
|
const problems = validate(SCHEMA, parsed).map(({ path: at, message }) => (at === "" ? message : `${at}: ${message}`));
|
|
47
48
|
if (problems.length > 0) {
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
// Claude Code reads it, and the skills directory, its lock and its links in step.
|
|
4
4
|
import { existsSync, globSync, lstatSync, readdirSync, readFileSync, readlinkSync, realpathSync, statSync } from "node:fs";
|
|
5
5
|
import { join, normalize } from "node:path";
|
|
6
|
-
import {
|
|
6
|
+
import { CannotRun, messageOf } from "../cannot-run.js";
|
|
7
7
|
import { codeSpans, tableRows } from "../markdown.js";
|
|
8
|
-
import {
|
|
8
|
+
import { optionsOf } from "./check.js";
|
|
9
9
|
import { overBudget } from "./markdown-budget.js";
|
|
10
10
|
import { catalogueFile } from "./primitives.js";
|
|
11
11
|
const ROUTER = "Task Router";
|
|
@@ -45,9 +45,8 @@ export function routes(text) {
|
|
|
45
45
|
}));
|
|
46
46
|
}
|
|
47
47
|
/** The budget, the Gate command, the row to the catalogue and every path the Task Router points at. */
|
|
48
|
-
function judgeMap(root, { file, ...budget }, text) {
|
|
48
|
+
function judgeMap(root, { file, ...budget }, text, gate) {
|
|
49
49
|
const findings = overBudget(text, budget).map((message) => ({ file, message, remedy: REMEDIES.budget }));
|
|
50
|
-
const { gate } = resolveConfig(root).config;
|
|
51
50
|
if (gate.command === undefined) {
|
|
52
51
|
findings.push({ message: "the Gate has no command for the map to name", remedy: REMEDIES.noCommand });
|
|
53
52
|
}
|
|
@@ -110,7 +109,7 @@ function lockedSkills(root) {
|
|
|
110
109
|
lock = JSON.parse(readFileSync(join(root, LOCK), "utf8"));
|
|
111
110
|
}
|
|
112
111
|
catch (error) {
|
|
113
|
-
return { problem: `not JSON: ${
|
|
112
|
+
return { problem: `not JSON: ${messageOf(error)}` };
|
|
114
113
|
}
|
|
115
114
|
if (typeof lock !== "object" || lock === null || !("skills" in lock)) {
|
|
116
115
|
return { problem: "it has no skills object" };
|
|
@@ -180,13 +179,13 @@ function linksTo(link, target) {
|
|
|
180
179
|
export function readMap(root, file) {
|
|
181
180
|
const path = join(root, file);
|
|
182
181
|
if (!existsSync(path)) {
|
|
183
|
-
throw new CannotRun(
|
|
182
|
+
throw new CannotRun("the map does not exist", REMEDIES.missing, { file });
|
|
184
183
|
}
|
|
185
184
|
return readFileSync(path, "utf8");
|
|
186
185
|
}
|
|
187
|
-
function run(root, options) {
|
|
186
|
+
function run(root, options, gate) {
|
|
188
187
|
return [
|
|
189
|
-
...judgeMap(root, options, readMap(root, options.file)),
|
|
188
|
+
...judgeMap(root, options, readMap(root, options.file), gate),
|
|
190
189
|
...judgeLink(root, options.file),
|
|
191
190
|
...judgeSkills(root)
|
|
192
191
|
];
|
package/dist/checks/check.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
+
import type { Gate } from "../agentic-config.ts";
|
|
2
|
+
import type { CannotRun } from "../cannot-run.ts";
|
|
1
3
|
import type { Failure } from "../report.ts";
|
|
2
4
|
/** A failure as a Check finds it: the file and line where it has one, what is wrong, the remedy. */
|
|
3
5
|
export type Finding = Omit<Failure, "rule">;
|
|
4
|
-
/**
|
|
5
|
-
export declare
|
|
6
|
-
finding: Finding;
|
|
7
|
-
constructor(finding: Finding);
|
|
8
|
-
}
|
|
6
|
+
/** What a Check reports when it cannot judge: the input it lacks, in the file it names, and the remedy. */
|
|
7
|
+
export declare function findingOf({ message, remedy, detail }: CannotRun, fallback: string): Finding;
|
|
9
8
|
/**
|
|
10
9
|
* A Ratchet's committed baseline: its file, and its refresh from the tree, which writes it tighter or
|
|
11
10
|
* records it where there is none, says what it did, and throws `CannotRun` when it cannot.
|
|
@@ -18,12 +17,15 @@ export type Check<Options extends object> = {
|
|
|
18
17
|
/** The Check's key under `gate.checks`. */
|
|
19
18
|
name: string;
|
|
20
19
|
defaults: Options;
|
|
21
|
-
/**
|
|
22
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Every failure of the tree at `root`, none when it passes; throws `CannotRun` when it cannot judge.
|
|
22
|
+
* `gate` is the resolved Gate it runs under, where it reads another Check's setting.
|
|
23
|
+
*/
|
|
24
|
+
run: (root: string, options: Options, gate: Gate) => Finding[];
|
|
23
25
|
/** Present on a Ratchet. */
|
|
24
26
|
baseline?: Baseline<Options>;
|
|
25
27
|
/** Present on a Check that derives files from their sources: writes them, says what it wrote. */
|
|
26
|
-
write?: (root: string, options: Options) => string;
|
|
28
|
+
write?: (root: string, options: Options, gate: Gate) => string;
|
|
27
29
|
};
|
|
28
30
|
/** The options `setting` turns the Check on with: the defaults under `true`, an object over them, else off. */
|
|
29
31
|
export declare function optionsOf<Options extends object>(check: Check<Options>, setting: unknown): Options | undefined;
|
package/dist/checks/check.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// What every Check the package ships is: a name, the options `true` stands for, and a run over the
|
|
2
2
|
// tree that answers with its failures. The Gate names the Check as each failure's rule.
|
|
3
|
-
/**
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
/** What a Check reports when it cannot judge: the input it lacks, in the file it names, and the remedy. */
|
|
4
|
+
export function findingOf({ message, remedy, detail }, fallback) {
|
|
5
|
+
const { file, line } = detail;
|
|
6
|
+
return {
|
|
7
|
+
...(file === undefined ? {} : { file }),
|
|
8
|
+
...(line === undefined ? {} : { line }),
|
|
9
|
+
message,
|
|
10
|
+
remedy: remedy ?? fallback
|
|
11
|
+
};
|
|
10
12
|
}
|
|
11
13
|
/** The options `setting` turns the Check on with: the defaults under `true`, an object over them, else off. */
|
|
12
14
|
export function optionsOf(check, setting) {
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// a frontmatter status from the allowed set and the frontmatter fields the config turns on.
|
|
4
4
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
5
5
|
import { join, matchesGlob, posix } from "node:path";
|
|
6
|
+
import { CannotRun } from "../cannot-run.js";
|
|
6
7
|
import { frontmatter, frontmatterList, headings } from "../markdown.js";
|
|
7
|
-
import {
|
|
8
|
+
import {} from "./check.js";
|
|
8
9
|
import { directoriesOf, treeFiles } from "./tree.js";
|
|
9
10
|
/** The frontmatter fields beyond `status` a record can be held to, in the order they are judged. */
|
|
10
11
|
const FIELDS = ["date", "touches", "superseded-by", "amended-by"];
|
|
@@ -201,7 +202,7 @@ function judge(root, dir, { statuses, fields }, tree) {
|
|
|
201
202
|
function run(root, options) {
|
|
202
203
|
const present = options.dirs.filter((dir) => isDirectory(join(root, dir)));
|
|
203
204
|
if (present.length === 0) {
|
|
204
|
-
throw new CannotRun(
|
|
205
|
+
throw new CannotRun(`none of ${options.dirs.join(", ")} exists`, REMEDIES.missing);
|
|
205
206
|
}
|
|
206
207
|
let cached;
|
|
207
208
|
const tree = () => {
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// within the count recorded there. A refresh only tightens the baseline: it lowers and drops, and adds no file.
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join, matchesGlob } from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { CannotRun, messageOf } from "../cannot-run.js";
|
|
6
|
+
import { asked } from "../git.js";
|
|
7
|
+
import {} from "./check.js";
|
|
7
8
|
import { matching, treeFiles } from "./tree.js";
|
|
8
9
|
const BASELINE = ".ai/file-size-ratchet.baseline.json";
|
|
9
10
|
const REFRESH = "dw-sdlc gate --update-baseline";
|
|
@@ -15,11 +16,11 @@ const REMEDIES = {
|
|
|
15
16
|
unreadable: "fix the failures it names"
|
|
16
17
|
};
|
|
17
18
|
/** A committed baseline gone from the tree: recording a new one would let every grown file back in. */
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
19
|
+
function deleted() {
|
|
20
|
+
return new CannotRun("the baseline is committed but gone from the tree", `restore it with git restore ${BASELINE}`, {
|
|
21
|
+
file: BASELINE
|
|
22
|
+
});
|
|
23
|
+
}
|
|
23
24
|
function show(value) {
|
|
24
25
|
return JSON.stringify(value);
|
|
25
26
|
}
|
|
@@ -58,7 +59,7 @@ function keyLines(text, path) {
|
|
|
58
59
|
function readBaseline(root) {
|
|
59
60
|
const path = join(root, BASELINE);
|
|
60
61
|
if (!existsSync(path)) {
|
|
61
|
-
const committed =
|
|
62
|
+
const committed = asked(root, ["cat-file", "-e", `HEAD:${BASELINE}`]) !== undefined;
|
|
62
63
|
return { state: "missing", committed };
|
|
63
64
|
}
|
|
64
65
|
const text = readFileSync(path, "utf8");
|
|
@@ -67,7 +68,7 @@ function readBaseline(root) {
|
|
|
67
68
|
parsed = JSON.parse(text);
|
|
68
69
|
}
|
|
69
70
|
catch (error) {
|
|
70
|
-
const message = `not JSON: ${
|
|
71
|
+
const message = `not JSON: ${messageOf(error)}`;
|
|
71
72
|
return { state: "invalid", findings: [{ file: BASELINE, message, remedy: REMEDIES.shape }] };
|
|
72
73
|
}
|
|
73
74
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
@@ -110,8 +111,7 @@ function contradictions(entries, groups) {
|
|
|
110
111
|
function run(root, { groups }) {
|
|
111
112
|
const read = readBaseline(root);
|
|
112
113
|
if (read.state === "missing") {
|
|
113
|
-
|
|
114
|
-
throw new CannotRun(read.committed ? DELETED : missing);
|
|
114
|
+
throw read.committed ? deleted() : new CannotRun("the baseline does not exist", REMEDIES.record, { file: BASELINE });
|
|
115
115
|
}
|
|
116
116
|
if (read.state === "invalid") {
|
|
117
117
|
return read.findings;
|
|
@@ -142,14 +142,10 @@ function write(root, entries) {
|
|
|
142
142
|
function refresh(root, { groups }) {
|
|
143
143
|
const read = readBaseline(root);
|
|
144
144
|
if (read.state === "invalid") {
|
|
145
|
-
throw new CannotRun({
|
|
146
|
-
file: BASELINE,
|
|
147
|
-
message: "it is not a baseline the Ratchet can read",
|
|
148
|
-
remedy: REMEDIES.unreadable
|
|
149
|
-
});
|
|
145
|
+
throw new CannotRun("it is not a baseline the Ratchet can read", REMEDIES.unreadable, { file: BASELINE });
|
|
150
146
|
}
|
|
151
147
|
if (read.state === "missing" && read.committed) {
|
|
152
|
-
throw
|
|
148
|
+
throw deleted();
|
|
153
149
|
}
|
|
154
150
|
const measured = measure(root, groups);
|
|
155
151
|
if (read.state === "missing") {
|
package/dist/checks/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { Gate } from "../agentic-config.ts";
|
|
1
2
|
import { type Finding } from "./check.ts";
|
|
2
|
-
/** A Check turned on by its setting, ready to judge a tree, and to refresh its baseline if it is a Ratchet. */
|
|
3
|
+
/** A Check turned on by its setting in a Gate, ready to judge a tree, and to refresh its baseline if it is a Ratchet. */
|
|
3
4
|
export type EnabledCheck = {
|
|
4
5
|
name: string;
|
|
5
6
|
run: (root: string) => Finding[];
|
|
@@ -11,7 +12,7 @@ export type EnabledCheck = {
|
|
|
11
12
|
};
|
|
12
13
|
/** The name of every Check the package ships, in the order the Gate runs them. */
|
|
13
14
|
export declare const CHECK_NAMES: readonly string[];
|
|
14
|
-
/** Every shipped Check
|
|
15
|
-
export declare function enabledChecks(
|
|
16
|
-
/** The shipped Check `name` on
|
|
17
|
-
export declare function shippedCheck(name: string,
|
|
15
|
+
/** Every shipped Check the resolved `gate` turns on with `true` or its options under `gate.checks`. */
|
|
16
|
+
export declare function enabledChecks(gate: Gate): EnabledCheck[];
|
|
17
|
+
/** The shipped Check `name` on the setting `gate` gives it, or on its defaults where the setting leaves it off. */
|
|
18
|
+
export declare function shippedCheck(name: string, gate: Gate): EnabledCheck | undefined;
|
package/dist/checks/index.js
CHANGED
|
@@ -15,19 +15,19 @@ import { temporalLanguage } from "./temporal-language.js";
|
|
|
15
15
|
function ship(check) {
|
|
16
16
|
return {
|
|
17
17
|
name: check.name,
|
|
18
|
-
enable: (setting) => {
|
|
18
|
+
enable: (setting, gate) => {
|
|
19
19
|
const options = optionsOf(check, setting);
|
|
20
20
|
if (options === undefined) {
|
|
21
21
|
return undefined;
|
|
22
22
|
}
|
|
23
|
-
const enabled = { name: check.name, run: (root) => check.run(root, options) };
|
|
23
|
+
const enabled = { name: check.name, run: (root) => check.run(root, options, gate) };
|
|
24
24
|
const { baseline } = check;
|
|
25
25
|
if (baseline !== undefined) {
|
|
26
26
|
enabled.baseline = { file: baseline.file, refresh: (root) => baseline.refresh(root, options) };
|
|
27
27
|
}
|
|
28
28
|
const { write } = check;
|
|
29
29
|
if (write !== undefined) {
|
|
30
|
-
enabled.write = (root) => write(root, options);
|
|
30
|
+
enabled.write = (root) => write(root, options, gate);
|
|
31
31
|
}
|
|
32
32
|
return enabled;
|
|
33
33
|
}
|
|
@@ -48,12 +48,13 @@ const SHIPPED = [
|
|
|
48
48
|
];
|
|
49
49
|
/** The name of every Check the package ships, in the order the Gate runs them. */
|
|
50
50
|
export const CHECK_NAMES = SHIPPED.map(({ name }) => name);
|
|
51
|
-
/** Every shipped Check
|
|
52
|
-
export function enabledChecks(
|
|
53
|
-
return SHIPPED.flatMap(({ name, enable }) => enable(checks[name]) ?? []);
|
|
51
|
+
/** Every shipped Check the resolved `gate` turns on with `true` or its options under `gate.checks`. */
|
|
52
|
+
export function enabledChecks(gate) {
|
|
53
|
+
return SHIPPED.flatMap(({ name, enable }) => enable(gate.checks[name], gate) ?? []);
|
|
54
54
|
}
|
|
55
|
-
/** The shipped Check `name` on
|
|
56
|
-
export function shippedCheck(name,
|
|
55
|
+
/** The shipped Check `name` on the setting `gate` gives it, or on its defaults where the setting leaves it off. */
|
|
56
|
+
export function shippedCheck(name, gate) {
|
|
57
|
+
const setting = gate.checks[name];
|
|
57
58
|
const on = setting === false || setting === undefined ? true : setting;
|
|
58
|
-
return SHIPPED.find((check) => check.name === name)?.enable(on);
|
|
59
|
+
return SHIPPED.find((check) => check.name === name)?.enable(on, gate);
|
|
59
60
|
}
|