@monte3l/groundwork 0.1.0-next.1 → 1.0.0-rc.3
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 +16 -8
- package/bin/m3l-groundwork.mjs +36 -2
- package/dist/assets.d.ts +10 -0
- package/dist/assets.js +14 -0
- package/dist/baseline-stage.d.ts +173 -0
- package/dist/baseline-stage.js +215 -0
- package/dist/caps.d.ts +4 -1
- package/dist/caps.js +17 -3
- package/dist/conflicts.d.ts +23 -2
- package/dist/conflicts.js +89 -11
- package/dist/customize-paths.d.ts +92 -0
- package/dist/customize-paths.js +115 -0
- package/dist/emit.d.ts +50 -1
- package/dist/emit.js +121 -13
- package/dist/fatal.d.ts +70 -0
- package/dist/fatal.js +132 -0
- package/dist/format-error.d.ts +113 -0
- package/dist/format-error.js +553 -0
- package/dist/fs-guard.d.ts +142 -0
- package/dist/fs-guard.js +222 -0
- package/dist/git.js +10 -1
- package/dist/harness/conformance.js +2 -0
- package/dist/harness/frontmatter.js +2 -13
- package/dist/harness/grade.js +37 -8
- package/dist/harness/rules.d.ts +20 -3
- package/dist/harness/rules.js +108 -6
- package/dist/harness/types.d.ts +13 -0
- package/dist/harness/types.js +3 -0
- package/dist/inventory.d.ts +77 -3
- package/dist/inventory.js +103 -12
- package/dist/jsonc.d.ts +49 -2
- package/dist/jsonc.js +140 -7
- package/dist/main.d.ts +62 -3
- package/dist/main.js +538 -67
- package/dist/merge-json.d.ts +47 -4
- package/dist/merge-json.js +120 -8
- package/dist/mode.js +12 -2
- package/dist/pack-stage.d.ts +210 -0
- package/dist/pack-stage.js +287 -0
- package/dist/packs.d.ts +19 -13
- package/dist/packs.js +231 -28
- package/dist/palette.d.ts +23 -0
- package/dist/palette.js +22 -0
- package/dist/plugin.d.ts +122 -6
- package/dist/plugin.js +687 -47
- package/dist/report.d.ts +21 -2
- package/dist/report.js +93 -5
- package/dist/staging.d.ts +176 -0
- package/dist/staging.js +375 -0
- package/dist/survey/fs-walk.d.ts +34 -2
- package/dist/survey/fs-walk.js +70 -5
- package/dist/survey/internal/blocked-path.d.ts +27 -0
- package/dist/survey/internal/blocked-path.js +129 -0
- package/dist/survey/internal/package-json.d.ts +13 -0
- package/dist/survey/internal/package-json.js +37 -0
- package/dist/survey/internal/read-guard.d.ts +178 -0
- package/dist/survey/internal/read-guard.js +276 -0
- package/dist/survey/survey-docs.d.ts +17 -2
- package/dist/survey/survey-docs.js +61 -21
- package/dist/survey/survey-harness.d.ts +20 -2
- package/dist/survey/survey-harness.js +87 -46
- package/dist/survey/survey-shape.d.ts +17 -2
- package/dist/survey/survey-shape.js +60 -46
- package/dist/survey/survey-toolchain.d.ts +16 -1
- package/dist/survey/survey-toolchain.js +69 -66
- package/dist/survey/survey.js +6 -4
- package/dist/survey/types.d.ts +79 -0
- package/dist/survey/types.js +2 -6
- package/dist/term.d.ts +80 -0
- package/dist/term.js +145 -0
- package/dist/tokens.js +2 -0
- package/dist/toolchain/conformance.js +2 -0
- package/dist/toolchain/grade.js +26 -8
- package/dist/toolchain/rules.d.ts +13 -4
- package/dist/toolchain/rules.js +16 -0
- package/dist/toolchain/tsconfig-chain.d.ts +2 -0
- package/dist/toolchain/tsconfig-chain.js +32 -8
- package/dist/toolchain/types.d.ts +16 -4
- package/dist/toolchain/types.js +3 -0
- package/package.json +4 -3
- package/plugin/skills/customize/SKILL.md +292 -18
- package/plugin/src/domain-map.ts +39 -14
- package/plugin/src/index.ts +5 -1
- package/plugin/src/kind-facet-map.ts +25 -8
- package/plugin/src/pack-map.ts +147 -25
- package/plugin/src/plugin-map.ts +236 -0
- package/templates/core/.claude/agents/Explore.md +0 -1
- package/templates/core/.claude/agents/code-implementer.md +4 -4
- package/templates/core/.claude/agents/code-reviewer.md +4 -4
- package/templates/core/.claude/agents/silent-failure-hunter.md +2 -2
- package/templates/core/.claude/agents/test-author.md +7 -5
- package/templates/core/.claude/hooks/guard-branch-isolation.mjs +56 -10
- package/templates/core/.claude/hooks/guard-double-background.mjs +13 -8
- package/templates/core/.claude/hooks/guard-git-push-signed.mjs +12 -9
- package/templates/core/.claude/hooks/guard-hub-src-writes.mjs +1890 -38
- package/templates/core/.claude/hooks/guard-js-extension.mjs +2 -1
- package/templates/core/.claude/hooks/guard-no-commonjs.mjs +7 -5
- package/templates/core/.claude/hooks/guard-secret-writes.mjs +7 -5
- package/templates/core/.claude/hooks/inject-decision-gate.mjs +12 -9
- package/templates/core/.claude/hooks/post-edit-verify.mjs +276 -98
- package/templates/core/.claude/rules/agent-dispatch.md +7 -0
- package/templates/core/.claude/rules/tests.md +2 -2
- package/templates/core/.claude/settings.json +5 -0
- package/templates/core/.claude/skills/finishing-work/SKILL.md +58 -10
- package/templates/core/.claude/skills/harness-guidance/SKILL.md +16 -7
- package/templates/core/.claude/skills/starting-work/SKILL.md +5 -0
- package/templates/core/.claude/skills/triaging-ci/SKILL.md +9 -8
- package/templates/core/.claude/skills/typescript-guidance/SKILL.md +137 -20
- package/templates/core/.claude/skills/typescript-guidance/references/area-catalog.md +135 -0
- package/templates/core/.claude/skills/typescript-guidance/references/tooling-sources.md +113 -0
- package/templates/core/.claude/skills/writing-commits/SKILL.md +2 -2
- package/templates/core/.github/dependabot.yml +18 -0
- package/templates/core/.github/workflows/ci.yml +15 -15
- package/templates/core/.github/workflows/dependency-review.yml +2 -2
- package/templates/core/.github/workflows/security-audit.yml +10 -4
- package/templates/core/.prettierignore +4 -0
- package/templates/core/CLAUDE.md +53 -3
- package/templates/core/README.md +30 -10
- package/templates/core/_gitignore +17 -0
- package/templates/core/bin/check-exports.mjs +11 -5
- package/templates/core/bin/lib/agent-roster.mjs +1 -1
- package/templates/core/bin/lib/frontmatter.mjs +4 -2
- package/templates/core/bin/lib/harness-rules.mjs +169 -20
- package/templates/core/bin/lib/protected-paths.mjs +93 -5
- package/templates/core/bin/lib/toolchain-rules.mjs +187 -26
- package/templates/core/bin/lib/verify-steps.mjs +29 -11
- package/templates/core/bin/verify.mjs +12 -0
- package/templates/core/eslint.config.js +5 -0
- package/templates/core/package.json +7 -7
- package/templates/core/vitest.config.ts +8 -1
- package/templates/packs/README.md +35 -24
- package/templates/packs/github/files/.claude/skills/reviewing-dependabot-prs/SKILL.md +133 -0
- package/templates/packs/github/files/.claude/skills/triaging-scan-alerts/SKILL.md +88 -0
- package/templates/packs/github/files/.claude/skills/watching-pr-checks/SKILL.md +94 -0
- package/templates/packs/github/files/.github/workflows/claude-pr-review.yml +267 -0
- package/templates/packs/github/files/.github/workflows/claude.yml +106 -0
- package/templates/packs/github/pack.json +19 -0
- package/templates/packs/harness-extras/files/.claude/hooks/guard-readonly-bash.mjs +1122 -65
- package/templates/packs/harness-extras/files/.claude/hooks/reinject-compact-handoff.mjs +147 -13
- package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/statusline.mjs +6 -2
- package/templates/packs/harness-extras/files/.claude/hooks/write-compact-handoff.mjs +227 -44
- package/templates/packs/harness-extras/pack.json +18 -13
- package/templates/packs/publishing/files/.changeset/README.md +25 -0
- package/templates/packs/publishing/files/.changeset/config.json +7 -0
- package/templates/packs/publishing/files/.github/release-tools/package.json +9 -0
- package/templates/packs/publishing/files/.github/workflows/release.yml +293 -0
- package/templates/packs/publishing/files/REUSE.toml +28 -0
- package/templates/packs/publishing/files/bin/check-dts-deps.mjs +234 -0
- package/templates/packs/publishing/files/bin/check-license-headers.mjs +217 -0
- package/templates/packs/publishing/files/bin/check-publish-version.mjs +149 -0
- package/templates/packs/publishing/files/bin/lib/npm-publish-args.mjs +86 -0
- package/templates/packs/publishing/files/bin/pnpm-publish-shim.mjs +86 -0
- package/templates/packs/publishing/pack.json +35 -0
- package/templates/packs/{harness-extras → quality}/files/bin/check-file-budget.mjs +6 -4
- package/templates/packs/quality/pack.json +29 -0
- package/templates/packs/supply-chain/files/.github/workflows/gitleaks.yml +52 -0
- package/templates/packs/supply-chain/files/.github/workflows/scorecard.yml +48 -0
- package/templates/packs/supply-chain/files/.gitleaks.toml +2 -0
- package/templates/packs/supply-chain/pack.json +19 -0
- package/templates/packs/worktrees/files/.claude/hooks/ensure-worktree-deps.mjs +283 -0
- package/templates/packs/worktrees/files/.claude/hooks/guard-worktree-only.mjs +245 -0
- package/templates/packs/worktrees/files/.claude/hooks/repair-core-bare.mjs +335 -0
- package/templates/packs/worktrees/files/.claude/skills/working-in-worktrees/SKILL.md +139 -0
- package/templates/packs/worktrees/files/.worktreeinclude +11 -0
- package/templates/packs/worktrees/pack.json +64 -0
- package/templates/packs/statusline/pack.json +0 -31
- /package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/statusline-layout.mjs +0 -0
- /package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/subagent-statusline.mjs +0 -0
- /package/templates/packs/{harness-extras → quality}/files/.claude/agents/type-design-analyzer.md +0 -0
- /package/templates/packs/{harness-extras → quality}/files/bin/file-budget-baseline.json +0 -0
|
@@ -17,19 +17,20 @@
|
|
|
17
17
|
* not a JavaScript tokenizer; it can mis-handle a regex literal or a template
|
|
18
18
|
* string, which none of the graded baseline files contain.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* Also used by m3l-groundwork's own adopt mode (the tool that generated this
|
|
21
|
+
* project's toolchain); if you're contributing a change back upstream, keep
|
|
22
|
+
* this file's behavior in sync with its source at
|
|
23
|
+
* `packages/cli/src/toolchain/{rules,grade}.ts` there.
|
|
23
24
|
*/
|
|
24
|
-
import {
|
|
25
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
25
26
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* Options TypeScript 6.0 deprecated. `removedIn` is the major that drops them
|
|
29
30
|
* (`outFile` and `moduleResolution: classic` already went in 6.0).
|
|
30
31
|
* https://www.typescriptlang.org/docs/handbook/release-notes/typescript-6-0.html
|
|
31
|
-
* @public
|
|
32
|
-
* m3l-groundwork
|
|
32
|
+
* @public Not imported anywhere else in this project -- exported only for
|
|
33
|
+
* m3l-groundwork's own upstream parity check (see the file header above).
|
|
33
34
|
*/
|
|
34
35
|
export const LEGACY_OPTIONS = [
|
|
35
36
|
{
|
|
@@ -113,8 +114,8 @@ export const LEGACY_OPTIONS = [
|
|
|
113
114
|
* (`@typescript-eslint/no-unused-vars` covers both with an `^_` escape hatch
|
|
114
115
|
* tsc's flags lack), and `forceConsistentCasingInFileNames` (defaults on).
|
|
115
116
|
* `allowUnreachableCode` is graded separately -- it must be `false`.
|
|
116
|
-
* @public
|
|
117
|
-
* m3l-groundwork
|
|
117
|
+
* @public Not imported anywhere else in this project -- exported only for
|
|
118
|
+
* m3l-groundwork's own upstream parity check (see the file header above).
|
|
118
119
|
*/
|
|
119
120
|
export const STRICT_FLAGS = [
|
|
120
121
|
"strict",
|
|
@@ -132,8 +133,8 @@ export const STRICT_FLAGS = [
|
|
|
132
133
|
|
|
133
134
|
/**
|
|
134
135
|
* The rubric categories, in report order.
|
|
135
|
-
* @public
|
|
136
|
-
* m3l-groundwork
|
|
136
|
+
* @public Not imported anywhere else in this project -- exported only for
|
|
137
|
+
* m3l-groundwork's own upstream parity check (see the file header above).
|
|
137
138
|
*/
|
|
138
139
|
export const CATEGORIES = [
|
|
139
140
|
"tsconfig",
|
|
@@ -207,6 +208,13 @@ const SKIP_DIR_NAMES = new Set([
|
|
|
207
208
|
".nx",
|
|
208
209
|
]);
|
|
209
210
|
|
|
211
|
+
// Claude Code creates git worktrees at `.claude/worktrees/<name>/` -- each a
|
|
212
|
+
// full second checkout that must not be walked twice. Matched as an exact
|
|
213
|
+
// path relative to the walk root, never by bare name: a directory literally
|
|
214
|
+
// named `worktrees` elsewhere (`src/worktrees/`, `.claude/skills/worktrees/`)
|
|
215
|
+
// is real project content and must stay visible to every grade.
|
|
216
|
+
const SKIP_REL_DIR_PATHS = new Set([".claude/worktrees"]);
|
|
217
|
+
|
|
210
218
|
const isRecord = (value) =>
|
|
211
219
|
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
212
220
|
const lower = (value) =>
|
|
@@ -228,9 +236,11 @@ function walkBounded(root, maxDepth) {
|
|
|
228
236
|
for (const entry of entries) {
|
|
229
237
|
if (entry.isDirectory() && SKIP_DIR_NAMES.has(entry.name)) continue;
|
|
230
238
|
const path = join(dir, entry.name);
|
|
239
|
+
const relPath = relative(root, path).split("\\").join("/");
|
|
240
|
+
if (entry.isDirectory() && SKIP_REL_DIR_PATHS.has(relPath)) continue;
|
|
231
241
|
results.push({
|
|
232
242
|
path,
|
|
233
|
-
relPath
|
|
243
|
+
relPath,
|
|
234
244
|
isDirectory: entry.isDirectory(),
|
|
235
245
|
});
|
|
236
246
|
if (entry.isDirectory()) visit(path, depth + 1);
|
|
@@ -293,13 +303,123 @@ function stripJsoncNoise(content) {
|
|
|
293
303
|
return result.replace(/,(\s*[}\]])/g, "$1");
|
|
294
304
|
}
|
|
295
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Strips `//` and block comments from JS/TS source, leaving `'`, `"` and
|
|
308
|
+
* `` ` `` string literals alone. Keeps trailing commas -- valid JS, and
|
|
309
|
+
* removing them is a JSON-only cleanup.
|
|
310
|
+
*
|
|
311
|
+
* A character scanner, not a parser: a regex literal containing a quote
|
|
312
|
+
* (`/["']/`) can be misread as opening a string, and a `${...}` expression
|
|
313
|
+
* inside a template literal containing its own backtick or quote can close
|
|
314
|
+
* the outer template early. Neither shape appears in this project's actual
|
|
315
|
+
* `eslint.config.js`/`vitest.config.ts`/`verify-steps.mjs` files.
|
|
316
|
+
*/
|
|
317
|
+
function stripJsComments(content) {
|
|
318
|
+
let result = "";
|
|
319
|
+
let quote;
|
|
320
|
+
let inLineComment = false;
|
|
321
|
+
let inBlockComment = false;
|
|
322
|
+
for (let i = 0; i < content.length; i++) {
|
|
323
|
+
const ch = content[i];
|
|
324
|
+
const next = content[i + 1];
|
|
325
|
+
if (inLineComment) {
|
|
326
|
+
if (ch === "\n") {
|
|
327
|
+
inLineComment = false;
|
|
328
|
+
result += ch;
|
|
329
|
+
}
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (inBlockComment) {
|
|
333
|
+
if (ch === "*" && next === "/") {
|
|
334
|
+
inBlockComment = false;
|
|
335
|
+
i++;
|
|
336
|
+
}
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (quote !== undefined) {
|
|
340
|
+
result += ch;
|
|
341
|
+
if (ch === "\\") {
|
|
342
|
+
result += next ?? "";
|
|
343
|
+
i++;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (ch === quote) quote = undefined;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
350
|
+
quote = ch;
|
|
351
|
+
result += ch;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (ch === "/" && next === "/") {
|
|
355
|
+
inLineComment = true;
|
|
356
|
+
i++;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (ch === "/" && next === "*") {
|
|
360
|
+
inBlockComment = true;
|
|
361
|
+
i++;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
result += ch;
|
|
365
|
+
}
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// --- errno classification (twin of packages/cli/src/survey/internal/read-guard.ts) ---
|
|
370
|
+
|
|
371
|
+
/** The `code` of a Node system error, or `undefined` for anything without a string `code`. */
|
|
372
|
+
function errnoCode(error) {
|
|
373
|
+
if (typeof error !== "object" || error === null) return undefined;
|
|
374
|
+
const code = Reflect.get(error, "code");
|
|
375
|
+
return typeof code === "string" ? code : undefined;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** "Nothing is at this path": nothing there, or an ancestor is a file. */
|
|
379
|
+
const ABSENT_CODES = new Set(["ENOENT", "ENOTDIR"]);
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* "Something is at this path this process cannot read or resolve": a
|
|
383
|
+
* permission failure, or a symlink loop. Never folded into "absent".
|
|
384
|
+
*/
|
|
385
|
+
const UNRESOLVABLE_CODES = new Set(["EACCES", "EPERM", "ELOOP"]);
|
|
386
|
+
|
|
387
|
+
const isAbsentError = (error) => ABSENT_CODES.has(errnoCode(error) ?? "");
|
|
388
|
+
|
|
389
|
+
function unresolvableCode(error) {
|
|
390
|
+
const code = errnoCode(error);
|
|
391
|
+
return code !== undefined && UNRESOLVABLE_CODES.has(code) ? code : undefined;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Reads and parses a JSONC file. A missing (`ENOENT`/`ENOTDIR`), unreadable
|
|
396
|
+
* (`EACCES`/`EPERM`), unresolvable (`ELOOP`), not-a-regular-file (`EISDIR`)
|
|
397
|
+
* or unparseable file is reported, not thrown. The existence check is a real
|
|
398
|
+
* `stat`, never `existsSync`, so a file under a directory this process cannot
|
|
399
|
+
* search is reported with its errno rather than as absent. Any other failure
|
|
400
|
+
* (`EIO`, `EMFILE`, ...) throws an `Error` naming the path, with the original
|
|
401
|
+
* as `cause`.
|
|
402
|
+
*/
|
|
296
403
|
function readJsonc(path) {
|
|
297
|
-
|
|
404
|
+
let content;
|
|
298
405
|
try {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
406
|
+
statSync(path);
|
|
407
|
+
content = readFileSync(path, "utf8");
|
|
408
|
+
} catch (error) {
|
|
409
|
+
if (isAbsentError(error)) {
|
|
410
|
+
return { ok: false, error: `${path} does not exist` };
|
|
411
|
+
}
|
|
412
|
+
if (errnoCode(error) === "EISDIR") {
|
|
413
|
+
return { ok: false, error: `${path} is not a regular file` };
|
|
414
|
+
}
|
|
415
|
+
const code = unresolvableCode(error);
|
|
416
|
+
if (code === undefined) {
|
|
417
|
+
throw new Error(`could not read ${path}`, { cause: error });
|
|
418
|
+
}
|
|
419
|
+
return { ok: false, error: `${path} is unreadable (${code})` };
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
return { ok: true, value: JSON.parse(stripJsoncNoise(content)) };
|
|
303
423
|
} catch (error) {
|
|
304
424
|
return {
|
|
305
425
|
ok: false,
|
|
@@ -311,7 +431,7 @@ function readJsonc(path) {
|
|
|
311
431
|
/** File text with comments stripped, or `undefined` when unreadable. */
|
|
312
432
|
function readSource(path) {
|
|
313
433
|
try {
|
|
314
|
-
return
|
|
434
|
+
return stripJsComments(readFileSync(path, "utf8"));
|
|
315
435
|
} catch {
|
|
316
436
|
return undefined;
|
|
317
437
|
}
|
|
@@ -326,12 +446,25 @@ function readRaw(path) {
|
|
|
326
446
|
}
|
|
327
447
|
}
|
|
328
448
|
|
|
449
|
+
/**
|
|
450
|
+
* Whether `path` is a candidate `extends` target: a regular file, or
|
|
451
|
+
* something this process cannot reach (`EACCES`/`EPERM`/`ELOOP`). The latter
|
|
452
|
+
* is taken as the target so `readJsonc` records it as unreadable with its
|
|
453
|
+
* errno -- never reported as resolving to no file when it may well be there.
|
|
454
|
+
* `ENOENT`/`ENOTDIR` is not a candidate; any other errno throws.
|
|
455
|
+
*/
|
|
329
456
|
function isFile(path) {
|
|
457
|
+
let stats;
|
|
330
458
|
try {
|
|
331
|
-
|
|
332
|
-
} catch {
|
|
333
|
-
return false;
|
|
459
|
+
stats = statSync(path);
|
|
460
|
+
} catch (error) {
|
|
461
|
+
if (isAbsentError(error)) return false;
|
|
462
|
+
if (unresolvableCode(error) !== undefined) return true;
|
|
463
|
+
throw new Error(`could not check whether ${path} exists`, {
|
|
464
|
+
cause: error,
|
|
465
|
+
});
|
|
334
466
|
}
|
|
467
|
+
return stats.isFile();
|
|
335
468
|
}
|
|
336
469
|
|
|
337
470
|
/** The `extends` value as a list: a string, or TypeScript 5.0+'s array form. */
|
|
@@ -469,20 +602,34 @@ function scrapeGateSteps(text) {
|
|
|
469
602
|
* Reads every `verify.mjs` invocation out of one YAML surface as data: the
|
|
470
603
|
* `--group`/`--step` each names. YAML is scraped, never parsed. An invocation
|
|
471
604
|
* naming neither (a matrix, or a bare full run) marks the surface `dynamic`.
|
|
605
|
+
* Comments are stripped per line before any `\` line continuation is joined,
|
|
606
|
+
* so a comment ending in `\` cannot swallow the next line. A quoted value is
|
|
607
|
+
* read unquoted. A mention inside an `echo` (in the same shell command, not an
|
|
608
|
+
* earlier `&&`/`;`/`|` segment) or inside a step's `name:` with no `run:`
|
|
609
|
+
* before it on the line is not an invocation.
|
|
472
610
|
*/
|
|
473
611
|
function scrapeLaneInvocations(text) {
|
|
474
612
|
const groups = [];
|
|
475
613
|
const steps = [];
|
|
476
614
|
let dynamic = false;
|
|
477
615
|
let seen = false;
|
|
478
|
-
|
|
479
|
-
|
|
616
|
+
const logicalLines = text
|
|
617
|
+
.split("\n")
|
|
618
|
+
.map((raw) => raw.replace(/(^|\s)#.*$/, ""))
|
|
619
|
+
.join("\n")
|
|
620
|
+
.replace(/\\\r?\n[ \t]*/g, " ")
|
|
621
|
+
.split("\n");
|
|
622
|
+
for (const line of logicalLines) {
|
|
480
623
|
const at = line.indexOf("verify.mjs");
|
|
481
624
|
if (at === -1) continue;
|
|
625
|
+
const prefix = line.slice(0, at);
|
|
626
|
+
const lastSegment = prefix.split(/&&|\|\||[;|]/).pop() ?? "";
|
|
627
|
+
if (/\becho\b/.test(lastSegment)) continue;
|
|
628
|
+
if (/\bname\s*:/.test(prefix) && !/\brun\s*:/.test(prefix)) continue;
|
|
482
629
|
seen = true;
|
|
483
630
|
const rest = line.slice(at);
|
|
484
|
-
const group = /--group[ =]+([A-Za-z][\w-]*)
|
|
485
|
-
const step = /--step[ =]+([A-Za-z][\w-]*)
|
|
631
|
+
const group = /--group[ =]+["']?([A-Za-z][\w-]*)["']?/.exec(rest);
|
|
632
|
+
const step = /--step[ =]+["']?([A-Za-z][\w-]*)["']?/.exec(rest);
|
|
486
633
|
if (group !== null) groups.push(group[1]);
|
|
487
634
|
else if (step !== null) steps.push(step[1]);
|
|
488
635
|
else dynamic = true;
|
|
@@ -657,6 +804,7 @@ const NONE = { checked: 0, failures: [] };
|
|
|
657
804
|
|
|
658
805
|
// --- structural rules ------------------------------------------------------
|
|
659
806
|
|
|
807
|
+
/** A tsconfig that fails to parse breaks every rule that reads its chain -- reporting the parse error itself, first, keeps a later rule's silence from being mistaken for a clean file. */
|
|
660
808
|
const tsconfigParses = {
|
|
661
809
|
id: "tsconfig-parses",
|
|
662
810
|
level: "structural",
|
|
@@ -672,6 +820,7 @@ const tsconfigParses = {
|
|
|
672
820
|
}),
|
|
673
821
|
};
|
|
674
822
|
|
|
823
|
+
/** An `extends` specifier that resolves to no file silently drops every option the base file would have set -- tsc itself gives no error until something downstream trips on the missing option. */
|
|
675
824
|
const tsconfigExtendsResolves = {
|
|
676
825
|
id: "tsconfig-extends-resolves",
|
|
677
826
|
level: "structural",
|
|
@@ -694,6 +843,7 @@ const tsconfigExtendsResolves = {
|
|
|
694
843
|
},
|
|
695
844
|
};
|
|
696
845
|
|
|
846
|
+
/** The tsconfig `build` compiles must actually set an outDir (or `build` emits .js next to sources) and must not set noEmit -- and if a separate tooling tsconfig sits beside it, that one must set noEmit, or typecheck starts emitting too. */
|
|
697
847
|
const tsconfigEmitCoherence = {
|
|
698
848
|
id: "tsconfig-emit-coherence",
|
|
699
849
|
level: "structural",
|
|
@@ -732,6 +882,7 @@ const tsconfigEmitCoherence = {
|
|
|
732
882
|
},
|
|
733
883
|
};
|
|
734
884
|
|
|
885
|
+
/** A verify step naming a package.json script, a file, or a group that doesn't exist is a wiring defect no offline check other than this one would catch before someone actually runs it. */
|
|
735
886
|
const gateWiring = {
|
|
736
887
|
id: "gate-wiring",
|
|
737
888
|
level: "structural",
|
|
@@ -800,6 +951,7 @@ const gateWiring = {
|
|
|
800
951
|
},
|
|
801
952
|
};
|
|
802
953
|
|
|
954
|
+
/** Confirms every CI/lefthook lane invokes verify.mjs by static group name, not by step id or a dynamic matrix -- otherwise a new gate can silently go unenforced in one surface while it passes in another. */
|
|
803
955
|
const gateLaneParity = {
|
|
804
956
|
id: "gate-lane-parity",
|
|
805
957
|
level: "structural",
|
|
@@ -853,6 +1005,7 @@ function enginesBounds(range) {
|
|
|
853
1005
|
};
|
|
854
1006
|
}
|
|
855
1007
|
|
|
1008
|
+
/** `.node-version` outside the range package.json's `engines.node` declares means the pinned dev/CI Node isn't even a Node version the project claims to support. */
|
|
856
1009
|
const nodePinCoherence = {
|
|
857
1010
|
id: "node-pin-coherence",
|
|
858
1011
|
level: "structural",
|
|
@@ -885,6 +1038,7 @@ const nodePinCoherence = {
|
|
|
885
1038
|
|
|
886
1039
|
// --- rubric rules ----------------------------------------------------------
|
|
887
1040
|
|
|
1041
|
+
/** The strict-family flags are the floor current TypeScript guidance recommends; tsc raises no warning for a flag simply left off, so this is the only check that notices. */
|
|
888
1042
|
const strictFlags = {
|
|
889
1043
|
id: "strict-flags",
|
|
890
1044
|
level: "rubric",
|
|
@@ -910,6 +1064,7 @@ const strictFlags = {
|
|
|
910
1064
|
},
|
|
911
1065
|
};
|
|
912
1066
|
|
|
1067
|
+
/** Warns ahead of a TypeScript major that removes an option outright -- by the time tsc itself rejects it, the fix is no longer a choice, it's an emergency. */
|
|
913
1068
|
const tsconfigOptionLifecycle = {
|
|
914
1069
|
id: "tsconfig-option-lifecycle",
|
|
915
1070
|
level: "rubric",
|
|
@@ -947,6 +1102,7 @@ const tsconfigOptionLifecycle = {
|
|
|
947
1102
|
},
|
|
948
1103
|
};
|
|
949
1104
|
|
|
1105
|
+
/** An outdated `module`/`moduleResolution`/`target` still compiles fine today, but forgoes current Node/bundler resolution semantics and syntax the project doesn't need to lower. */
|
|
950
1106
|
const moduleTargetModern = {
|
|
951
1107
|
id: "module-target-modern",
|
|
952
1108
|
level: "rubric",
|
|
@@ -989,6 +1145,7 @@ const moduleTargetModern = {
|
|
|
989
1145
|
},
|
|
990
1146
|
};
|
|
991
1147
|
|
|
1148
|
+
/** ESLint 10 reads only eslint.config.* -- a lingering legacy eslintrc file means either an old ESLint is still in play, or a current one is silently linting nothing. */
|
|
992
1149
|
const eslintFlatConfig = {
|
|
993
1150
|
id: "eslint-flat-config",
|
|
994
1151
|
level: "rubric",
|
|
@@ -1008,6 +1165,7 @@ const eslintFlatConfig = {
|
|
|
1008
1165
|
},
|
|
1009
1166
|
};
|
|
1010
1167
|
|
|
1168
|
+
/** Without a type-checked preset and `projectService`, typescript-eslint runs syntax-only rules and silently skips every rule that needs real type information. */
|
|
1011
1169
|
const eslintTypedLinting = {
|
|
1012
1170
|
id: "eslint-typed-linting",
|
|
1013
1171
|
level: "rubric",
|
|
@@ -1054,6 +1212,7 @@ const eslintTypedLinting = {
|
|
|
1054
1212
|
},
|
|
1055
1213
|
};
|
|
1056
1214
|
|
|
1215
|
+
/** `bin/**` and `.claude/hooks/**` are code this project actually runs, not incidental scripts -- if ESLint has no config block for them, or actively ignores src, they ship unlinted. */
|
|
1057
1216
|
const eslintCoversEmittedCode = {
|
|
1058
1217
|
id: "eslint-covers-emitted-code",
|
|
1059
1218
|
level: "rubric",
|
|
@@ -1093,6 +1252,7 @@ const eslintCoversEmittedCode = {
|
|
|
1093
1252
|
},
|
|
1094
1253
|
};
|
|
1095
1254
|
|
|
1255
|
+
/** A coverage threshold without `perFile: true` lets one well-tested file's coverage average out another file with none -- the gate passes while a whole file goes untested. */
|
|
1096
1256
|
const coverageGate = {
|
|
1097
1257
|
id: "coverage-gate",
|
|
1098
1258
|
level: "rubric",
|
|
@@ -1119,6 +1279,7 @@ const coverageGate = {
|
|
|
1119
1279
|
},
|
|
1120
1280
|
};
|
|
1121
1281
|
|
|
1282
|
+
/** An unpinned toolchain package, a missing `packageManager`, or an `@types/node` major that doesn't match the pinned Node version each make installs non-reproducible across machines and CI runs. */
|
|
1122
1283
|
const toolchainPinShape = {
|
|
1123
1284
|
id: "toolchain-pin-shape",
|
|
1124
1285
|
level: "rubric",
|
|
@@ -1172,8 +1333,8 @@ const toolchainPinShape = {
|
|
|
1172
1333
|
|
|
1173
1334
|
/**
|
|
1174
1335
|
* Every rule, structural first. Order is the order findings are reported in.
|
|
1175
|
-
* @public
|
|
1176
|
-
* m3l-groundwork
|
|
1336
|
+
* @public Not imported anywhere else in this project -- exported only for
|
|
1337
|
+
* m3l-groundwork's own upstream parity check (see the file header above).
|
|
1177
1338
|
*/
|
|
1178
1339
|
export const RULES = [
|
|
1179
1340
|
tsconfigParses,
|
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* The single source of truth for what `pnpm verify` runs locally, what the
|
|
4
4
|
* lefthook `pre-push` lanes run, and what each `.github/workflows/ci.yml`
|
|
5
|
-
* job runs.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* job runs.
|
|
6
|
+
*
|
|
7
|
+
* Two words, two meanings: a *step* is one gate (e.g. "Lint", cmd
|
|
8
|
+
* `pnpm lint`); a *group* is one of five fixed buckets a step belongs to
|
|
9
|
+
* (`format`/`lint`/`typecheck`/`build`/`test`, see GROUPS below). A *lane* is
|
|
10
|
+
* the outside caller that runs a whole group at once -- a lefthook
|
|
11
|
+
* `pre-push` lane, or a CI job in `.github/workflows/ci.yml`. Both YAML files
|
|
12
|
+
* name a *group*, never a step id: that's what lets a new step -- core or
|
|
13
|
+
* pack-contributed -- join `pnpm verify` without ever touching either YAML
|
|
14
|
+
* file, and what keeps this the one list to keep in sync instead of three
|
|
15
|
+
* that agree by hand.
|
|
10
16
|
*
|
|
11
17
|
* Pack-installed steps live in verify-steps.packs.json, a plain JSON array
|
|
12
18
|
* the bootstrapper CLI appends to when installing a pack -- it has no JS
|
|
@@ -25,9 +31,11 @@ import { fileURLToPath } from "node:url";
|
|
|
25
31
|
export const GROUPS = ["format", "lint", "typecheck", "build", "test"];
|
|
26
32
|
|
|
27
33
|
/**
|
|
28
|
-
* The gate steps this
|
|
29
|
-
*
|
|
30
|
-
*
|
|
34
|
+
* The gate steps this project ships out of the box, before any pack's own
|
|
35
|
+
* steps are appended.
|
|
36
|
+
* @public Not imported anywhere else in this project -- exported only so
|
|
37
|
+
* m3l-groundwork can compare it against its own upstream copy when it
|
|
38
|
+
* updates this tooling.
|
|
31
39
|
*/
|
|
32
40
|
export const CORE_STEPS = [
|
|
33
41
|
{
|
|
@@ -90,11 +98,21 @@ function readPackSteps() {
|
|
|
90
98
|
let raw;
|
|
91
99
|
try {
|
|
92
100
|
raw = readFileSync(path, "utf8");
|
|
93
|
-
} catch {
|
|
94
|
-
return [];
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (error?.code === "ENOENT") return [];
|
|
103
|
+
throw new Error(`verify-steps.packs.json: cannot read ${path}`, {
|
|
104
|
+
cause: error,
|
|
105
|
+
});
|
|
95
106
|
}
|
|
96
107
|
|
|
97
|
-
|
|
108
|
+
let parsed;
|
|
109
|
+
try {
|
|
110
|
+
parsed = JSON.parse(raw);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
throw new Error(`verify-steps.packs.json: failed to parse ${path}`, {
|
|
113
|
+
cause: error,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
98
116
|
if (!Array.isArray(parsed)) {
|
|
99
117
|
throw new Error("verify-steps.packs.json must be a JSON array of steps");
|
|
100
118
|
}
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
* `lefthook.yml` each invoke `--group <name>`, never individual step ids, so
|
|
8
8
|
* a new step -- core or pack-contributed -- is picked up by both without
|
|
9
9
|
* either file changing.
|
|
10
|
+
*
|
|
11
|
+
* "Group" and "lane" are two different things: a group is one of the five
|
|
12
|
+
* fixed buckets above; a lane is the outside caller that runs a whole group
|
|
13
|
+
* at once -- one lefthook `pre-push` lane, or one CI job in `ci.yml`.
|
|
10
14
|
*/
|
|
11
15
|
import process from "node:process";
|
|
12
16
|
import { spawnSync } from "node:child_process";
|
|
@@ -19,8 +23,16 @@ import {
|
|
|
19
23
|
|
|
20
24
|
const args = process.argv.slice(2);
|
|
21
25
|
const stepIndex = args.indexOf("--step");
|
|
26
|
+
if (stepIndex !== -1 && !args[stepIndex + 1]) {
|
|
27
|
+
console.error("verify: --step requires a value");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
22
30
|
const requestedId = stepIndex === -1 ? null : args[stepIndex + 1];
|
|
23
31
|
const groupIndex = args.indexOf("--group");
|
|
32
|
+
if (groupIndex !== -1 && !args[groupIndex + 1]) {
|
|
33
|
+
console.error("verify: --group requires a value");
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
24
36
|
const requestedGroup = groupIndex === -1 ? null : args[groupIndex + 1];
|
|
25
37
|
|
|
26
38
|
if (requestedGroup && !GROUPS.includes(requestedGroup)) {
|
|
@@ -22,6 +22,11 @@ export default defineConfig(
|
|
|
22
22
|
".claude/agents/**",
|
|
23
23
|
".claude/skills/**",
|
|
24
24
|
".claude/rules/**",
|
|
25
|
+
// A worktree the harness creates (see .prettierignore) is a full,
|
|
26
|
+
// independent checkout under here, with its own eslint-relevant files
|
|
27
|
+
// -- without this, `pnpm lint` from the main checkout also walks (and
|
|
28
|
+
// can fail on) that copy's own in-progress state.
|
|
29
|
+
".claude/worktrees/**",
|
|
25
30
|
],
|
|
26
31
|
},
|
|
27
32
|
js.configs.recommended,
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "
|
|
7
|
-
"packageManager": "pnpm@12.
|
|
6
|
+
"description": "TODO: describe __PROJECT_NAME__ (bootstrapped by m3l-groundwork).",
|
|
7
|
+
"packageManager": "pnpm@12.8.1",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24"
|
|
10
10
|
},
|
|
@@ -41,18 +41,18 @@
|
|
|
41
41
|
"@commitlint/types": "^21.2.0",
|
|
42
42
|
"@eslint/js": "^10.0.1",
|
|
43
43
|
"@types/node": "^24.13.3",
|
|
44
|
-
"@vitest/coverage-v8": "^
|
|
45
|
-
"eslint": "^10.
|
|
44
|
+
"@vitest/coverage-v8": "^5.0.2",
|
|
45
|
+
"eslint": "^10.11.0",
|
|
46
46
|
"eslint-import-resolver-typescript": "^4.4.5",
|
|
47
47
|
"eslint-plugin-import-x": "^4.17.1",
|
|
48
48
|
"eslint-plugin-tsdoc": "^0.5.2",
|
|
49
49
|
"globals": "^17.12.0",
|
|
50
|
-
"knip": "^6.
|
|
50
|
+
"knip": "^6.38.0",
|
|
51
51
|
"lefthook": "^2.1.12",
|
|
52
52
|
"prettier": "^3.9.6",
|
|
53
53
|
"publint": "^0.3.24",
|
|
54
54
|
"typescript": "^6.0.3",
|
|
55
|
-
"typescript-eslint": "^8.
|
|
56
|
-
"vitest": "^
|
|
55
|
+
"typescript-eslint": "^8.71.0",
|
|
56
|
+
"vitest": "^5.0.2"
|
|
57
57
|
}
|
|
58
58
|
}
|
|
@@ -4,7 +4,14 @@ export default defineConfig({
|
|
|
4
4
|
test: {
|
|
5
5
|
pool: "forks",
|
|
6
6
|
include: ["**/tests/**/*.test.ts", "**/*.test.ts"],
|
|
7
|
-
exclude: [
|
|
7
|
+
exclude: [
|
|
8
|
+
"**/dist/**",
|
|
9
|
+
"**/node_modules/**",
|
|
10
|
+
// A worktree the harness creates (see .prettierignore) is a full,
|
|
11
|
+
// independent checkout under here, with its own tests/ tree --
|
|
12
|
+
// without this every test in the project runs twice, once per copy.
|
|
13
|
+
"**/.claude/worktrees/**",
|
|
14
|
+
],
|
|
8
15
|
coverage: {
|
|
9
16
|
provider: "v8",
|
|
10
17
|
include: ["src/**/*.ts"],
|
|
@@ -18,24 +18,33 @@ templates/packs/<name>/
|
|
|
18
18
|
|
|
19
19
|
## The wiring contract
|
|
20
20
|
|
|
21
|
-
**A pack never edits YAML or JavaScript.** It may add files under `files
|
|
22
|
-
|
|
21
|
+
**A pack never edits YAML or JavaScript.** It may add files under `files/`.
|
|
22
|
+
**A pack also can't ship a `.claude/rules/*.md` file**: the harness grader's
|
|
23
|
+
structural `claudemd-refs` rule fails on any rule `CLAUDE.md` doesn't name
|
|
24
|
+
by path, and a pack has no way to edit `CLAUDE.md` (that's not one of the
|
|
25
|
+
three JSON files below either). If a pack needs to state a project-wide
|
|
26
|
+
policy, put it in the body of a skill it ships instead — see the `worktrees`
|
|
27
|
+
pack's `working-in-worktrees` skill for the pattern.
|
|
28
|
+
It may also extend three JSON files the baseline already reads at runtime:
|
|
23
29
|
`.claude/settings.json` (hook registrations, and top-level harness settings
|
|
24
30
|
such as `statusLine`), `package.json` (`scripts`), and
|
|
25
|
-
`bin/lib/verify-steps.packs.json
|
|
26
|
-
fixed verify groups
|
|
27
|
-
`
|
|
31
|
+
`bin/lib/verify-steps.packs.json`. That last file holds gate steps, each
|
|
32
|
+
keyed to one of the five fixed verify groups
|
|
33
|
+
`templates/core/bin/lib/verify-steps.mjs` defines —
|
|
34
|
+
`format`/`lint`/`typecheck`/`build`/`test`. A gate registered this way runs
|
|
28
35
|
under `pnpm verify`, every `lefthook.yml` `pre-push` lane, and every
|
|
29
|
-
`.github/workflows/ci.yml` job automatically
|
|
30
|
-
enumerate groups rather than individual steps.
|
|
36
|
+
`.github/workflows/ci.yml` job automatically. That works because all three
|
|
37
|
+
already enumerate groups rather than individual steps.
|
|
31
38
|
|
|
32
39
|
`pack.json` fields:
|
|
33
40
|
|
|
34
41
|
- `schemaVersion` — currently `1`.
|
|
35
|
-
- `modes` — `["fresh"]` and/or `["fresh", "adopt"]`.
|
|
36
|
-
dependency on the baseline's exact file layout (an
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
- `modes` — `["fresh"]` and/or `["fresh", "adopt"]`. "Adopt-capable" means
|
|
43
|
+
an artifact has no dependency on the baseline's exact file layout (an
|
|
44
|
+
agent, most hooks), so it's safe to install into an already-existing
|
|
45
|
+
project's own layout. A gate that assumes a specific source layout isn't
|
|
46
|
+
adopt-capable -- it should say so honestly in `adoptNotes` instead of
|
|
47
|
+
claiming `adopt`.
|
|
39
48
|
- `budget` — the pack's cap deltas (`agents`/`skills`/`hooks`/`workflows`/
|
|
40
49
|
`scripts`), checked against `templates/core`'s own counts by a structural
|
|
41
50
|
test, never against `templates/core` + every other pack.
|
|
@@ -63,19 +72,21 @@ enumerate groups rather than individual steps.
|
|
|
63
72
|
repeatable) — it wrote the baseline moments ago, so there's no uncertainty
|
|
64
73
|
to defer.
|
|
65
74
|
- **Adopt mode**: the CLI never installs a pack. It surveys which packs
|
|
66
|
-
apply and stages their payload at `.groundwork/packs/<name
|
|
67
|
-
`/customize`
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
75
|
+
apply and stages their payload at `.groundwork/packs/<name>/`. Later,
|
|
76
|
+
`/customize` confirms the install in **Step 0** (its first, up-front
|
|
77
|
+
confirmation round, before any file is written) and performs it in
|
|
78
|
+
**Round 1** (the first pass of deterministic edits that follows). That
|
|
79
|
+
installation translates `wiring.verifySteps`/`wiring.settings` against
|
|
80
|
+
the project's _real_ gate runner and hook config — read for real by that
|
|
81
|
+
point, not guessed at offline.
|
|
71
82
|
|
|
72
83
|
## Available packs
|
|
73
84
|
|
|
74
|
-
| Pack | Contents
|
|
75
|
-
| ---------------- |
|
|
76
|
-
| `harness-extras` |
|
|
77
|
-
| `
|
|
78
|
-
|
|
79
|
-
`
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
| Pack | Contents |
|
|
86
|
+
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
87
|
+
| `harness-extras` | Claude Code session ergonomics: the compaction-handoff hook pair, a read-only Bash guard, and a five-row status line (session, model, context, quota, work) plus a per-subagent row renderer. The only pack that sets top-level settings keys (`statusLine`, `subagentStatusLine`). |
|
|
88
|
+
| `quality` | Two language-level review aids: a per-file size ratchet gate (`check-file-budget.mjs`, a `build`-group verify step) and a read-only `type-design-analyzer` agent. No hooks, no settings. Adopt-capable and recommended for every project kind. |
|
|
89
|
+
| `github` | GitHub-hosted collaboration: Anthropic's official Claude Code GitHub Action wired for `@claude` mention-mode, a second Action that posts an automated Claude review comment on every PR, and three `gh`-CLI skills — `reviewing-dependabot-prs`, `triaging-scan-alerts`, `watching-pr-checks`. No hooks, no gate. The two workflows need an auth secret this pack cannot create; see its `adoptNotes`. |
|
|
90
|
+
| `publishing` | A release pipeline: `release.yml` (changesets version-PR / staged, provenance-attested npm publish via trusted publishing), `check-publish-version.mjs`, `check-dts-deps.mjs`, `check-license-headers.mjs` and a `REUSE.toml` template. Fresh mode only — see "Install path" below and its `adoptNotes`. |
|
|
91
|
+
| `supply-chain` | Secret scanning (`gitleaks.yml` with a `.gitleaks.toml`) and an OpenSSF Scorecard run (`scorecard.yml`) for any project on GitHub, published or not. A pure file drop of two read-only workflows: no hooks, no settings, no scripts, no gate. Adopt-capable and recommended for every project kind. |
|
|
92
|
+
| `worktrees` | Enforces that all src/tests development happens inside an isolated git worktree, on any branch, for any caller: a `working-in-worktrees` skill (start/status/sync/finish/fan-out), a `SessionStart` hook that installs dependencies into a freshly created worktree, a `PreToolUse` guard stricter than the baseline's own `guard-branch-isolation.mjs`/`guard-hub-src-writes.mjs`, a `repair-core-bare.mjs` hook (`SessionStart`, and `PostToolUse` after `EnterWorktree`/`ExitWorktree`/`Agent`) that resets the `core.bare = true` Claude Code's worktree tools are reported to leave in a normal repo's shared config, and a `.worktreeinclude` copying `.env`/`.env.local`/`.env.*.local` into every worktree Claude Code creates. Changes the day-to-day workflow, not just a nicety — see its `adoptNotes`. |
|