@monte3l/groundwork 1.0.0-rc.3 → 1.0.0-rc.5

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.
Files changed (36) hide show
  1. package/dist/customize-paths.d.ts +51 -15
  2. package/dist/customize-paths.js +32 -13
  3. package/dist/fs-guard.d.ts +2 -2
  4. package/dist/harness/rules.d.ts +2 -1
  5. package/dist/harness/rules.js +3 -1
  6. package/dist/main.js +36 -0
  7. package/dist/packs.d.ts +3 -1
  8. package/dist/packs.js +11 -1
  9. package/dist/plugin.d.ts +12 -12
  10. package/dist/plugin.js +34 -26
  11. package/package.json +1 -1
  12. package/plugin/skills/customize/SKILL.md +33 -403
  13. package/plugin/skills/customize/step-0-reconcile.md +261 -0
  14. package/plugin/skills/customize/step-3-round-1.md +170 -0
  15. package/plugin/src/plugin-map.ts +131 -10
  16. package/templates/core/.claude/agents/Explore.md +1 -1
  17. package/templates/core/.claude/hooks/guard-no-commonjs.mjs +3 -2
  18. package/templates/core/.claude/hooks/inject-decision-gate.mjs +7 -7
  19. package/templates/core/.claude/skills/triaging-ci/SKILL.md +2 -2
  20. package/templates/core/.claude/skills/writing-commits/SKILL.md +4 -4
  21. package/templates/core/.prettierignore +1 -0
  22. package/templates/core/_gitignore +1 -0
  23. package/templates/core/bin/check-exports.mjs +5 -3
  24. package/templates/core/bin/lib/harness-rules.mjs +3 -1
  25. package/templates/core/tsconfig.base.json +10 -6
  26. package/templates/packs/README.md +15 -0
  27. package/templates/packs/github/files/.github/workflows/claude-pr-review.yml +1 -1
  28. package/templates/packs/github/files/.github/workflows/claude.yml +1 -1
  29. package/templates/packs/github/pack.json +1 -1
  30. package/templates/packs/harness-extras/files/.claude/hooks/subagent-statusline.mjs +29 -14
  31. package/templates/packs/harness-extras/pack.json +1 -1
  32. package/templates/packs/publishing/files/.github/release-tools/package.json +1 -1
  33. package/templates/packs/publishing/files/.github/workflows/release.yml +8 -6
  34. package/templates/packs/publishing/pack.json +7 -1
  35. package/templates/packs/quality/files/.claude/agents/type-design-analyzer.md +1 -1
  36. package/templates/packs/quality/pack.json +1 -1
@@ -1,7 +1,8 @@
1
1
  /**
2
- * The skill's entry file: the one payload file that lives under the plugin
3
- * source's `skills/customize/` (every other one lives under its `src/`), and
4
- * the one whose presence makes Claude Code load the skill -- which is why
2
+ * The skill's entry file: it lives under the plugin source's
3
+ * `skills/customize/` beside {@link CUSTOMIZE_SKILL_STEP_FILE_NAMES} (the
4
+ * backing-data files live under its `src/`), and it is the one payload file
5
+ * whose presence makes Claude Code load the skill -- which is why
5
6
  * {@link CUSTOMIZE_SKILL_WRITE_ORDER} writes it last.
6
7
  *
7
8
  * @example
@@ -12,6 +13,24 @@
12
13
  * ```
13
14
  */
14
15
  export declare const CUSTOMIZE_SKILL_ENTRY_FILE = "SKILL.md";
16
+ /**
17
+ * The step files `SKILL.md` delegates to (Step 0's adopt-mode reconcile and
18
+ * Step 3's Round 1), sourced from the plugin's `skills/customize/` beside
19
+ * {@link CUSTOMIZE_SKILL_ENTRY_FILE}. Like the backing-data files, they are
20
+ * written before `SKILL.md`, so a loadable `SKILL.md` never sits beside a
21
+ * missing step file.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * import { CUSTOMIZE_SKILL_STEP_FILE_NAMES } from "./customize-paths.js";
26
+ *
27
+ * CUSTOMIZE_SKILL_STEP_FILE_NAMES[0]; // "step-0-reconcile.md"
28
+ * ```
29
+ */
30
+ export declare const CUSTOMIZE_SKILL_STEP_FILE_NAMES: readonly [
31
+ "step-0-reconcile.md",
32
+ "step-3-round-1.md"
33
+ ];
15
34
  /**
16
35
  * Every payload file's name inside an installed copy of the skill.
17
36
  *
@@ -22,19 +41,28 @@ export declare const CUSTOMIZE_SKILL_ENTRY_FILE = "SKILL.md";
22
41
  * CUSTOMIZE_SKILL_FILE_NAMES.includes("SKILL.md"); // true
23
42
  * ```
24
43
  */
25
- export declare const CUSTOMIZE_SKILL_FILE_NAMES: readonly ["SKILL.md", "kind-facet-map.ts", "domain-map.ts", "pack-map.ts", "plugin-map.ts"];
44
+ export declare const CUSTOMIZE_SKILL_FILE_NAMES: readonly [
45
+ "SKILL.md",
46
+ "kind-facet-map.ts",
47
+ "domain-map.ts",
48
+ "pack-map.ts",
49
+ "plugin-map.ts",
50
+ "step-0-reconcile.md",
51
+ "step-3-round-1.md"
52
+ ];
26
53
  /**
27
- * The order an install writes the payload in: every backing-data file
28
- * first, {@link CUSTOMIZE_SKILL_ENTRY_FILE} last. A run that fails part-way
29
- * (and whose rollback cannot remove everything) therefore never leaves a
30
- * loadable `SKILL.md` beside missing or stale data files -- given the
31
- * precondition that no `SKILL.md` already sits at the destination. Wherever
32
- * an install replaces existing entries -- fresh mode's
54
+ * The order an install writes the payload in: every backing-data file, then
55
+ * every step file, {@link CUSTOMIZE_SKILL_ENTRY_FILE} last. A run that fails
56
+ * part-way (and whose rollback cannot remove everything) therefore never
57
+ * leaves a loadable `SKILL.md` beside missing or stale data or step files --
58
+ * given the precondition that no `SKILL.md` already sits at the destination.
59
+ * Wherever an install replaces existing entries -- fresh mode's
33
60
  * `.claude/skills/customize/` (a `--force` re-run over an earlier install)
34
61
  * and the CLI-owned `.groundwork/customize/` -- `plugin.ts` establishes that
35
62
  * precondition by removing any existing `SKILL.md` entry before the first
36
- * data file is rewritten. Adopt mode's additive `.claude/skills/customize/`
37
- * install only ever writes there when no `SKILL.md` entry exists.
63
+ * data or step file is rewritten. Adopt mode's additive
64
+ * `.claude/skills/customize/` install only ever writes there when no
65
+ * `SKILL.md` entry exists.
38
66
  *
39
67
  * @example
40
68
  * ```ts
@@ -43,7 +71,15 @@ export declare const CUSTOMIZE_SKILL_FILE_NAMES: readonly ["SKILL.md", "kind-fac
43
71
  * CUSTOMIZE_SKILL_WRITE_ORDER.at(-1); // "SKILL.md"
44
72
  * ```
45
73
  */
46
- export declare const CUSTOMIZE_SKILL_WRITE_ORDER: readonly ["kind-facet-map.ts", "domain-map.ts", "pack-map.ts", "plugin-map.ts", "SKILL.md"];
74
+ export declare const CUSTOMIZE_SKILL_WRITE_ORDER: readonly [
75
+ "kind-facet-map.ts",
76
+ "domain-map.ts",
77
+ "pack-map.ts",
78
+ "plugin-map.ts",
79
+ "step-0-reconcile.md",
80
+ "step-3-round-1.md",
81
+ "SKILL.md"
82
+ ];
47
83
  /**
48
84
  * The fresh-mode destination -- and adopt mode's, when nothing is there yet
49
85
  * or an earlier install was interrupted before its `SKILL.md` -- as path
@@ -63,7 +99,7 @@ export declare const CLAUDE_DEST_SEGMENTS: readonly [".claude", "skills", "custo
63
99
  * project root. Used whenever {@link CLAUDE_DEST_SEGMENTS} cannot be written
64
100
  * additively: a project-owned entry under any of the skill's payload names
65
101
  * that is not this CLI's current copy (a differing file, a symlink, a
66
- * directory, a `SKILL.md` without its data), or a component of
102
+ * directory, a `SKILL.md` without its data or step files), or a component of
67
103
  * `.claude/skills/customize` that is a symlink or not a directory.
68
104
  *
69
105
  * @example
@@ -85,7 +121,7 @@ export declare const GROUNDWORK_DEST_SEGMENTS: readonly [".groundwork", "customi
85
121
  * import { plannedCustomizeSkillPaths } from "./customize-paths.js";
86
122
  *
87
123
  * plannedCustomizeSkillPaths();
88
- * // [".claude/skills/customize/SKILL.md", ..., ".groundwork/customize/plugin-map.ts"]
124
+ * // [".claude/skills/customize/SKILL.md", ..., ".groundwork/customize/step-3-round-1.md"]
89
125
  * ```
90
126
  */
91
127
  export declare function plannedCustomizeSkillPaths(): readonly string[];
@@ -7,9 +7,10 @@
7
7
  */
8
8
  import { join } from "node:path";
9
9
  /**
10
- * The skill's entry file: the one payload file that lives under the plugin
11
- * source's `skills/customize/` (every other one lives under its `src/`), and
12
- * the one whose presence makes Claude Code load the skill -- which is why
10
+ * The skill's entry file: it lives under the plugin source's
11
+ * `skills/customize/` beside {@link CUSTOMIZE_SKILL_STEP_FILE_NAMES} (the
12
+ * backing-data files live under its `src/`), and it is the one payload file
13
+ * whose presence makes Claude Code load the skill -- which is why
13
14
  * {@link CUSTOMIZE_SKILL_WRITE_ORDER} writes it last.
14
15
  *
15
16
  * @example
@@ -27,6 +28,21 @@ const CUSTOMIZE_SKILL_DATA_FILE_NAMES = [
27
28
  "pack-map.ts",
28
29
  "plugin-map.ts",
29
30
  ];
31
+ /**
32
+ * The step files `SKILL.md` delegates to (Step 0's adopt-mode reconcile and
33
+ * Step 3's Round 1), sourced from the plugin's `skills/customize/` beside
34
+ * {@link CUSTOMIZE_SKILL_ENTRY_FILE}. Like the backing-data files, they are
35
+ * written before `SKILL.md`, so a loadable `SKILL.md` never sits beside a
36
+ * missing step file.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * import { CUSTOMIZE_SKILL_STEP_FILE_NAMES } from "./customize-paths.js";
41
+ *
42
+ * CUSTOMIZE_SKILL_STEP_FILE_NAMES[0]; // "step-0-reconcile.md"
43
+ * ```
44
+ */
45
+ export const CUSTOMIZE_SKILL_STEP_FILE_NAMES = ["step-0-reconcile.md", "step-3-round-1.md"];
30
46
  /**
31
47
  * Every payload file's name inside an installed copy of the skill.
32
48
  *
@@ -40,19 +56,21 @@ const CUSTOMIZE_SKILL_DATA_FILE_NAMES = [
40
56
  export const CUSTOMIZE_SKILL_FILE_NAMES = [
41
57
  CUSTOMIZE_SKILL_ENTRY_FILE,
42
58
  ...CUSTOMIZE_SKILL_DATA_FILE_NAMES,
59
+ ...CUSTOMIZE_SKILL_STEP_FILE_NAMES,
43
60
  ];
44
61
  /**
45
- * The order an install writes the payload in: every backing-data file
46
- * first, {@link CUSTOMIZE_SKILL_ENTRY_FILE} last. A run that fails part-way
47
- * (and whose rollback cannot remove everything) therefore never leaves a
48
- * loadable `SKILL.md` beside missing or stale data files -- given the
49
- * precondition that no `SKILL.md` already sits at the destination. Wherever
50
- * an install replaces existing entries -- fresh mode's
62
+ * The order an install writes the payload in: every backing-data file, then
63
+ * every step file, {@link CUSTOMIZE_SKILL_ENTRY_FILE} last. A run that fails
64
+ * part-way (and whose rollback cannot remove everything) therefore never
65
+ * leaves a loadable `SKILL.md` beside missing or stale data or step files --
66
+ * given the precondition that no `SKILL.md` already sits at the destination.
67
+ * Wherever an install replaces existing entries -- fresh mode's
51
68
  * `.claude/skills/customize/` (a `--force` re-run over an earlier install)
52
69
  * and the CLI-owned `.groundwork/customize/` -- `plugin.ts` establishes that
53
70
  * precondition by removing any existing `SKILL.md` entry before the first
54
- * data file is rewritten. Adopt mode's additive `.claude/skills/customize/`
55
- * install only ever writes there when no `SKILL.md` entry exists.
71
+ * data or step file is rewritten. Adopt mode's additive
72
+ * `.claude/skills/customize/` install only ever writes there when no
73
+ * `SKILL.md` entry exists.
56
74
  *
57
75
  * @example
58
76
  * ```ts
@@ -63,6 +81,7 @@ export const CUSTOMIZE_SKILL_FILE_NAMES = [
63
81
  */
64
82
  export const CUSTOMIZE_SKILL_WRITE_ORDER = [
65
83
  ...CUSTOMIZE_SKILL_DATA_FILE_NAMES,
84
+ ...CUSTOMIZE_SKILL_STEP_FILE_NAMES,
66
85
  CUSTOMIZE_SKILL_ENTRY_FILE,
67
86
  ];
68
87
  /**
@@ -84,7 +103,7 @@ export const CLAUDE_DEST_SEGMENTS = [".claude", "skills", "customize"];
84
103
  * project root. Used whenever {@link CLAUDE_DEST_SEGMENTS} cannot be written
85
104
  * additively: a project-owned entry under any of the skill's payload names
86
105
  * that is not this CLI's current copy (a differing file, a symlink, a
87
- * directory, a `SKILL.md` without its data), or a component of
106
+ * directory, a `SKILL.md` without its data or step files), or a component of
88
107
  * `.claude/skills/customize` that is a symlink or not a directory.
89
108
  *
90
109
  * @example
@@ -106,7 +125,7 @@ export const GROUNDWORK_DEST_SEGMENTS = [".groundwork", "customize"];
106
125
  * import { plannedCustomizeSkillPaths } from "./customize-paths.js";
107
126
  *
108
127
  * plannedCustomizeSkillPaths();
109
- * // [".claude/skills/customize/SKILL.md", ..., ".groundwork/customize/plugin-map.ts"]
128
+ * // [".claude/skills/customize/SKILL.md", ..., ".groundwork/customize/step-3-round-1.md"]
110
129
  * ```
111
130
  */
112
131
  export function plannedCustomizeSkillPaths() {
@@ -12,7 +12,7 @@
12
12
  * endsWithRerunAdvice(message); // true
13
13
  * ```
14
14
  */
15
- export declare const FIX_AND_RERUN_ADVICE = "fix the cause and re-run the CLI";
15
+ export declare const FIX_AND_RERUN_ADVICE: string;
16
16
  /**
17
17
  * Whether `message` already ENDS with "re-run the CLI" advice, in any
18
18
  * wording that ends that way (e.g. {@link assertNotSymlink}'s "remove it and
@@ -138,5 +138,5 @@ export declare function assertNotDirectory(path: string, advice: string): void;
138
138
  * assertNotSymlink("/work/app/package.json", FRESH_SYMLINK_ADVICE);
139
139
  * ```
140
140
  */
141
- export declare const FRESH_SYMLINK_ADVICE = "remove it, then retry the same command with --fresh --force added";
141
+ export declare const FRESH_SYMLINK_ADVICE: string;
142
142
  //# sourceMappingURL=fs-guard.d.ts.map
@@ -59,7 +59,8 @@ export interface HarnessRule {
59
59
  * Model ids and aliases the rubric accepts: the current ids and aliases, plus
60
60
  * ids that were once listed here, kept until Anthropic deprecates them. The
61
61
  * ids follow Anthropic's models overview and model-deprecations pages
62
- * (retrieved 2026-10-01). A legacy id that was never listed here is
62
+ * (retrieved 2026-10-08): Haiku 5.5 (released 2026-10-07) is current, and
63
+ * Haiku 4.5 is legacy but still active. A legacy id that was never listed here is
63
64
  * deliberately not added, so the rule keeps nudging pins toward current
64
65
  * models. Bump alongside the `harness-guidance` refresh sweep;
65
66
  * `templates/core/bin/lib/harness-rules.mjs` carries the same list and the
@@ -20,7 +20,8 @@ import { fieldList, fieldText, parseFrontmatter } from "./frontmatter.js";
20
20
  * Model ids and aliases the rubric accepts: the current ids and aliases, plus
21
21
  * ids that were once listed here, kept until Anthropic deprecates them. The
22
22
  * ids follow Anthropic's models overview and model-deprecations pages
23
- * (retrieved 2026-10-01). A legacy id that was never listed here is
23
+ * (retrieved 2026-10-08): Haiku 5.5 (released 2026-10-07) is current, and
24
+ * Haiku 4.5 is legacy but still active. A legacy id that was never listed here is
24
25
  * deliberately not added, so the rule keeps nudging pins toward current
25
26
  * models. Bump alongside the `harness-guidance` refresh sweep;
26
27
  * `templates/core/bin/lib/harness-rules.mjs` carries the same list and the
@@ -37,6 +38,7 @@ export const CURRENT_MODELS = [
37
38
  "claude-sonnet-5",
38
39
  "claude-sonnet-5-5",
39
40
  "claude-fable-5-1",
41
+ "claude-haiku-5-5",
40
42
  "claude-haiku-4-5",
41
43
  "claude-haiku-4-5-20251001",
42
44
  ];
package/dist/main.js CHANGED
@@ -242,6 +242,27 @@ export function formatCapsSummary(baseline, installed) {
242
242
  lines.push(`= ${formatCounts(total)}${overCap.length > 0 ? ` ⚠ over cap: ${overCap.join(", ")}` : ""}`);
243
243
  return { text: lines.join("\n"), overCap: overCap.length > 0 };
244
244
  }
245
+ /**
246
+ * Fresh mode's closing "setup required" block: one heading, then each pack
247
+ * that declares `setupSteps` (in `packs` order) followed by its commands,
248
+ * each indented two spaces. `undefined` when no pack declares any, so the
249
+ * caller prints nothing extra.
250
+ */
251
+ function formatSetupBlock(targetDir, packs) {
252
+ const lines = [];
253
+ for (const { manifest } of packs) {
254
+ const steps = manifest.setupSteps ?? [];
255
+ if (steps.length === 0)
256
+ continue;
257
+ lines.push(`pack "${manifest.name}":`, ...steps.map((step) => ` ${step}`));
258
+ }
259
+ if (lines.length === 0)
260
+ return undefined;
261
+ return [
262
+ `\nthe installed pack(s) need setup before the first \`pnpm verify\` -- run these in ${targetDir}:`,
263
+ ...lines,
264
+ ].join("\n");
265
+ }
245
266
  /**
246
267
  * Fresh mode's pre-flight over the `/customize` skill's own destination
247
268
  * (`.claude/skills/customize/`): each directory component must be missing
@@ -295,6 +316,18 @@ function runFresh(options, platform) {
295
316
  : summary.text);
296
317
  }
297
318
  const { targetDir, skipInstall, projectName } = options;
319
+ // Built once, now that the project and its packs are on disk, and printed
320
+ // exactly once per run: at the very end on success, or just before the
321
+ // throw when git init / pnpm install fails -- a plain re-run of a
322
+ // non-empty target adopts it, and adopt mode never prints setupSteps. The
323
+ // /customize-skill failure below deliberately does not print it: its
324
+ // message says to re-run with --fresh --force, which prints it on success.
325
+ const setupBlock = formatSetupBlock(targetDir, packs);
326
+ const printSetupBlock = () => {
327
+ if (setupBlock !== undefined) {
328
+ console.log(paint(process.stdout, "warning", setupBlock));
329
+ }
330
+ };
298
331
  let pluginResult;
299
332
  try {
300
333
  pluginResult = installCustomizeSkill(targetDir);
@@ -314,6 +347,7 @@ function runFresh(options, platform) {
314
347
  gitInit(targetDir);
315
348
  }
316
349
  catch (error) {
350
+ printSetupBlock();
317
351
  throw new Error(`git init failed, but the project was written to ${targetDir}; run \`git init\`${skipInstall ? "" : " and `pnpm install`"} there yourself`, { cause: error });
318
352
  }
319
353
  console.log("initialized git repository");
@@ -323,11 +357,13 @@ function runFresh(options, platform) {
323
357
  }
324
358
  catch (error) {
325
359
  console.log(paint(process.stdout, "warning", `\n${projectName} written to ${targetDir}, but dependencies are not installed`));
360
+ printSetupBlock();
326
361
  throw new Error(`${describeInstallFailure(error)}; the project was written to ${targetDir} -- run \`pnpm install\` there yourself to finish`, { cause: error });
327
362
  }
328
363
  console.log("installed dependencies");
329
364
  }
330
365
  console.log(paint(process.stdout, "success", `\n✓ ${projectName} is ready at ${targetDir}`));
366
+ printSetupBlock();
331
367
  }
332
368
  /**
333
369
  * Explains why the post-emission `pnpm install` failed: a missing binary
package/dist/packs.d.ts CHANGED
@@ -19,6 +19,8 @@ export interface PackManifest {
19
19
  } | undefined;
20
20
  wiring: PackWiring;
21
21
  adoptNotes: string | undefined;
22
+ /** Shell commands, run in the target directory, that a fresh install needs before its first `pnpm verify` (fresh mode prints them; see main.ts). Optional: absent means no setup. */
23
+ setupSteps?: string[] | undefined;
22
24
  }
23
25
  export interface Pack {
24
26
  manifest: PackManifest;
@@ -28,7 +30,7 @@ export interface Pack {
28
30
  export declare function packsRootDir(): string;
29
31
  /** Names of every pack directory that has a `pack.json` under `root`, sorted for deterministic install order. `root` defaults to `templates/packs`, overridable for tests. */
30
32
  export declare function listPackNames(root?: string): string[];
31
- /** Loads and validates one pack's manifest from under `root` (default `templates/packs`, overridable for tests). Throws, naming the available packs, if unknown; throws naming the pack and the problem if malformed, including a prototype-sensitive key in its wiring (see {@link assertValidWiringShape}). */
33
+ /** Loads and validates one pack's manifest from under `root` (default `templates/packs`, overridable for tests). Throws, naming the available packs, if unknown; throws naming the pack and the problem if malformed, including a prototype-sensitive key in its wiring (see {@link assertValidWiringShape}) or a `setupSteps` that is present but not a non-empty array of single-line, non-empty strings. */
32
34
  export declare function loadPack(name: string, root?: string): Pack;
33
35
  export interface PackInstallResult {
34
36
  filesWritten: string[];
package/dist/packs.js CHANGED
@@ -132,7 +132,13 @@ function assertValidWiringShape(name, wiring) {
132
132
  assertValidVerifyStep(name, step, index);
133
133
  });
134
134
  }
135
- /** Loads and validates one pack's manifest from under `root` (default `templates/packs`, overridable for tests). Throws, naming the available packs, if unknown; throws naming the pack and the problem if malformed, including a prototype-sensitive key in its wiring (see {@link assertValidWiringShape}). */
135
+ /** True when `steps` is a non-empty array of non-empty strings, none containing a line break -- each is printed as one indented line. */
136
+ function isValidSetupSteps(steps) {
137
+ return (Array.isArray(steps) &&
138
+ steps.length > 0 &&
139
+ steps.every((step) => typeof step === "string" && step !== "" && !/[\r\n]/.test(step)));
140
+ }
141
+ /** Loads and validates one pack's manifest from under `root` (default `templates/packs`, overridable for tests). Throws, naming the available packs, if unknown; throws naming the pack and the problem if malformed, including a prototype-sensitive key in its wiring (see {@link assertValidWiringShape}) or a `setupSteps` that is present but not a non-empty array of single-line, non-empty strings. */
136
142
  export function loadPack(name, root = packsRootDir()) {
137
143
  const packDir = join(root, name);
138
144
  const manifestPath = join(packDir, "pack.json");
@@ -176,6 +182,10 @@ export function loadPack(name, root = packsRootDir()) {
176
182
  if (!isValidBudget(budget)) {
177
183
  throw new Error(`pack "${name}": pack.json's budget must set ${CAP_KEYS.join(", ")} to non-negative integers`);
178
184
  }
185
+ const setupSteps = manifest.setupSteps;
186
+ if (setupSteps !== undefined && !isValidSetupSteps(setupSteps)) {
187
+ throw new Error(`pack "${name}": pack.json's setupSteps must be a non-empty array of single-line, non-empty strings`);
188
+ }
179
189
  // pack-stage.ts's stagePacks keys each pack's staging directory on
180
190
  // manifest.name, so a mismatch would let two pack directories collide.
181
191
  if (manifestName !== name) {
package/dist/plugin.d.ts CHANGED
@@ -89,20 +89,20 @@ export interface GuardedInstallResult {
89
89
  * `fallbackCause` is `"component"` -- the entry (and any link target) is
90
90
  * left untouched.
91
91
  * - Otherwise, if every payload file already there is a regular file
92
- * matching what this CLI ships byte-for-byte: with all five present the
93
- * result is `"already-present"` (nothing written); with no `SKILL.md` file
94
- * (none at all, or an install interrupted before writing it) the missing
95
- * files are written into `.claude/skills/customize/`, `SKILL.md` last,
96
- * never rewriting or removing the correct ones.
92
+ * matching what this CLI ships byte-for-byte: with every payload file
93
+ * present the result is `"already-present"` (nothing written); with no
94
+ * `SKILL.md` file (none at all, or an install interrupted before writing
95
+ * it) the missing files are written into `.claude/skills/customize/`,
96
+ * `SKILL.md` last, never rewriting or removing the correct ones.
97
97
  * - Anything else under a payload name (a differing file, a regular file
98
98
  * that cannot be read, a symlink -- dangling or not -- a directory, a
99
- * `SKILL.md` without its data; detected by `lstat`) is kept as the
100
- * project's own: the skill is written to `.groundwork/customize/` instead,
101
- * `fallbackReason` names that entry (saying so when it could not be read)
102
- * and `fallbackCause` is `"entry"`. Adopt mode never guesses at a project
103
- * entry it cannot compare, and the fallback does not guess either: it
104
- * leaves that entry exactly as it was. Claude Code does not load a skill
105
- * from there; the caller must say so.
99
+ * `SKILL.md` without its data or step files; detected by `lstat`) is kept
100
+ * as the project's own: the skill is written to `.groundwork/customize/`
101
+ * instead, `fallbackReason` names that entry (saying so when it could not
102
+ * be read) and `fallbackCause` is `"entry"`. Adopt mode never guesses at a
103
+ * project entry it cannot compare, and the fallback does not guess either:
104
+ * it leaves that entry exactly as it was. Claude Code does not load a
105
+ * skill from there; the caller must say so.
106
106
  *
107
107
  * Writes into `.claude/skills/customize/` use `"wx"` only, so an entry that
108
108
  * appears there mid-install fails the run (rolled back) rather than being
package/dist/plugin.js CHANGED
@@ -4,14 +4,14 @@
4
4
  * Installs the `/customize` skill into a bootstrapped or adopted project.
5
5
  * Claude Code's marketplace-based plugin installation is an interactive,
6
6
  * network-involving flow this offline CLI can't drive; instead this copies
7
- * the skill's SKILL.md plus its deterministic backing data directly into a
8
- * destination directory, so `/customize` works immediately with no further
9
- * setup step.
7
+ * the skill's SKILL.md, its step files and its deterministic backing data
8
+ * into a destination directory, so `/customize` works immediately with no
9
+ * further setup step.
10
10
  */
11
11
  import { lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
12
12
  import { join } from "node:path";
13
13
  import { resolveAsset } from "./assets.js";
14
- import { CLAUDE_DEST_SEGMENTS, CUSTOMIZE_SKILL_ENTRY_FILE, CUSTOMIZE_SKILL_WRITE_ORDER, GROUNDWORK_DEST_SEGMENTS, } from "./customize-paths.js";
14
+ import { CLAUDE_DEST_SEGMENTS, CUSTOMIZE_SKILL_ENTRY_FILE, CUSTOMIZE_SKILL_STEP_FILE_NAMES, CUSTOMIZE_SKILL_WRITE_ORDER, GROUNDWORK_DEST_SEGMENTS, } from "./customize-paths.js";
15
15
  import { assertNotSymlink, endsWithRerunAdvice, FIX_AND_RERUN_ADVICE, FRESH_RETRY, FRESH_SYMLINK_ADVICE, } from "./fs-guard.js";
16
16
  /** Resolves the plugin payload for a source checkout (`packages/plugin`) or a published tarball (`plugin/`). */
17
17
  function pluginDir() {
@@ -107,6 +107,14 @@ function errnoField(error, field) {
107
107
  const value = Reflect.get(error, field);
108
108
  return typeof value === "string" ? value : undefined;
109
109
  }
110
+ /**
111
+ * The payload files sourced from the plugin's `skills/customize/` (the
112
+ * entry file and its step files); every other one comes from its `src/`.
113
+ */
114
+ const SKILL_DIR_FILE_NAMES = new Set([
115
+ CUSTOMIZE_SKILL_ENTRY_FILE,
116
+ ...CUSTOMIZE_SKILL_STEP_FILE_NAMES,
117
+ ]);
110
118
  /**
111
119
  * Reads every payload file from the plugin source, in
112
120
  * {@link CUSTOMIZE_SKILL_WRITE_ORDER} (`SKILL.md` last), before anything is
@@ -117,7 +125,7 @@ function errnoField(error, field) {
117
125
  */
118
126
  function readCustomizeSkillPayload(sourceDir) {
119
127
  return CUSTOMIZE_SKILL_WRITE_ORDER.map((name) => {
120
- const from = name === CUSTOMIZE_SKILL_ENTRY_FILE
128
+ const from = SKILL_DIR_FILE_NAMES.has(name)
121
129
  ? join(sourceDir, "skills", "customize", name)
122
130
  : join(sourceDir, "src", name);
123
131
  try {
@@ -201,13 +209,13 @@ function assertNoDirectoryAtPayloadNames(destDir, payload, alreadyCurrent, polic
201
209
  /**
202
210
  * For every policy that replaces existing entries: removes an existing
203
211
  * `SKILL.md` entry (a file or a symlink, unlinked, never followed) BEFORE any
204
- * data file is rewritten, so a later failure never leaves a stale `SKILL.md`
205
- * loadable beside missing or half-rewritten data. Runs after
206
- * {@link assertNoDirectoryAtPayloadNames}, so a directory there has already
207
- * been refused (one raced in since makes `rmSync` throw). Returns the
208
- * removed path, if any; throws (wrapped) before anything is written when the
209
- * removal fails. `label` is how the entry is described: `"existing"` for a
210
- * project's own `.claude/` copy, `"stale"` for the CLI-owned staging.
212
+ * data or step file is rewritten, so a later failure never leaves a stale
213
+ * `SKILL.md` loadable beside missing or half-rewritten data or step files.
214
+ * Runs after {@link assertNoDirectoryAtPayloadNames}, so a directory there
215
+ * has already been refused (one raced in since makes `rmSync` throw). Returns
216
+ * the removed path, if any; throws (wrapped) before anything is written when
217
+ * the removal fails. `label` is how the entry is described: `"existing"` for
218
+ * a project's own `.claude/` copy, `"stale"` for the CLI-owned staging.
211
219
  */
212
220
  function removeStaleSkillEntry(destDir, label) {
213
221
  const staleEntry = join(destDir, CUSTOMIZE_SKILL_ENTRY_FILE);
@@ -323,8 +331,8 @@ function replacedClause(policy, skillRemoved, replaced, skillMaybeLeft) {
323
331
  if (skillRemoved === undefined && others === "") {
324
332
  return "";
325
333
  }
326
- // One sentence for SKILL.md and the data files alike: every one of
327
- // them was removed and none is restored.
334
+ // One sentence for SKILL.md and the other payload files alike: every
335
+ // one of them was removed and none is restored.
328
336
  const removedPaths = [
329
337
  ...(skillRemoved === undefined
330
338
  ? []
@@ -668,20 +676,20 @@ function installGuarded(targetDir, sourceDir) {
668
676
  * `fallbackCause` is `"component"` -- the entry (and any link target) is
669
677
  * left untouched.
670
678
  * - Otherwise, if every payload file already there is a regular file
671
- * matching what this CLI ships byte-for-byte: with all five present the
672
- * result is `"already-present"` (nothing written); with no `SKILL.md` file
673
- * (none at all, or an install interrupted before writing it) the missing
674
- * files are written into `.claude/skills/customize/`, `SKILL.md` last,
675
- * never rewriting or removing the correct ones.
679
+ * matching what this CLI ships byte-for-byte: with every payload file
680
+ * present the result is `"already-present"` (nothing written); with no
681
+ * `SKILL.md` file (none at all, or an install interrupted before writing
682
+ * it) the missing files are written into `.claude/skills/customize/`,
683
+ * `SKILL.md` last, never rewriting or removing the correct ones.
676
684
  * - Anything else under a payload name (a differing file, a regular file
677
685
  * that cannot be read, a symlink -- dangling or not -- a directory, a
678
- * `SKILL.md` without its data; detected by `lstat`) is kept as the
679
- * project's own: the skill is written to `.groundwork/customize/` instead,
680
- * `fallbackReason` names that entry (saying so when it could not be read)
681
- * and `fallbackCause` is `"entry"`. Adopt mode never guesses at a project
682
- * entry it cannot compare, and the fallback does not guess either: it
683
- * leaves that entry exactly as it was. Claude Code does not load a skill
684
- * from there; the caller must say so.
686
+ * `SKILL.md` without its data or step files; detected by `lstat`) is kept
687
+ * as the project's own: the skill is written to `.groundwork/customize/`
688
+ * instead, `fallbackReason` names that entry (saying so when it could not
689
+ * be read) and `fallbackCause` is `"entry"`. Adopt mode never guesses at a
690
+ * project entry it cannot compare, and the fallback does not guess either:
691
+ * it leaves that entry exactly as it was. Claude Code does not load a
692
+ * skill from there; the caller must say so.
685
693
  *
686
694
  * Writes into `.claude/skills/customize/` use `"wx"` only, so an entry that
687
695
  * appears there mid-install fails the run (rolled back) rather than being
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monte3l/groundwork",
3
- "version": "1.0.0-rc.3",
3
+ "version": "1.0.0-rc.5",
4
4
  "description": "CLI that writes a TypeScript toolchain and Claude Code setup into an empty directory, or -- against an existing project -- read-only surveys it and reports the differences. No prompts, and no network access beyond the package install.",
5
5
  "keywords": [
6
6
  "typescript",