@houseofwolvesllc/claude-scrum-skill 2.1.1 → 2.1.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "claude-scrum-skill",
3
3
  "description": "Complete scrum pipeline — PRD to production release with Claude as scrum master. Includes project scaffolding, sprint planning, status tracking, sprint releases, and full-project emulation testing.",
4
- "version": "2.1.1",
4
+ "version": "2.1.3",
5
5
  "author": {
6
6
  "name": "House of Wolves LLC"
7
7
  },
package/bin/install.js CHANGED
@@ -6,25 +6,9 @@ const path = require('path');
6
6
  const HOME = process.env.HOME || process.env.USERPROFILE;
7
7
  const SOURCE_DIR = path.join(__dirname, '..', 'skills');
8
8
  const WORKFLOWS_SOURCE_DIR = path.join(__dirname, '..', 'lib', 'workflows');
9
+ const GUIDANCE_SOURCE_DIR = path.join(__dirname, '..', 'lib', 'guidance');
9
10
  const IS_GLOBAL = process.env.npm_config_global === 'true';
10
-
11
- // Global install → ~/.claude/skills/
12
- // Local install → <project>/.claude/skills/
13
- let skillsDir;
14
- if (IS_GLOBAL) {
15
- skillsDir = path.join(HOME, '.claude', 'skills');
16
- } else {
17
- // Walk up from node_modules to find the project root
18
- let projectRoot = path.resolve(__dirname, '..');
19
- while (projectRoot !== path.dirname(projectRoot)) {
20
- if (path.basename(projectRoot) === 'node_modules') {
21
- projectRoot = path.dirname(projectRoot);
22
- break;
23
- }
24
- projectRoot = path.dirname(projectRoot);
25
- }
26
- skillsDir = path.join(projectRoot, '.claude', 'skills');
27
- }
11
+ const CONFIG_FILENAME = 'config.json';
28
12
 
29
13
  const skills = [
30
14
  'project-scaffold',
@@ -37,76 +21,207 @@ const skills = [
37
21
  'project-cleanup'
38
22
  ];
39
23
 
40
- function copyRecursive(src, dest) {
41
- if (!fs.existsSync(src)) return;
24
+ function main() {
25
+ const skillsDir = resolveSkillsDir();
26
+ const location = IS_GLOBAL ? 'global (~/.claude/skills/)' : `project (${skillsDir})`;
27
+ console.log(`\n📋 Installing claude-scrum-skill (${location})...\n`);
42
28
 
43
- if (fs.statSync(src).isDirectory()) {
44
- fs.mkdirSync(dest, { recursive: true });
45
- for (const item of fs.readdirSync(src)) {
46
- copyRecursive(path.join(src, item), path.join(dest, item));
47
- }
48
- } else {
49
- fs.copyFileSync(src, dest);
29
+ fs.mkdirSync(skillsDir, { recursive: true });
30
+
31
+ installSharedReferences(skillsDir);
32
+ const installed = installSkills(skillsDir);
33
+ installWorkflows(skillsDir);
34
+ installGuidance(skillsDir);
35
+
36
+ if (!IS_GLOBAL) {
37
+ ensureGitignoreEntry(skillsDir);
50
38
  }
39
+
40
+ console.log(`\n✨ Installed ${installed} skills to ${skillsDir}`);
41
+ console.log(' Restart Claude Code for the skills to become available.\n');
42
+ console.log(' Run /project-scaffold <prd-path> to get started.\n');
51
43
  }
52
44
 
53
- const location = IS_GLOBAL ? 'global (~/.claude/skills/)' : `project (${skillsDir})`;
54
- console.log(`\n📋 Installing claude-scrum-skill (${location})...\n`);
45
+ function resolveSkillsDir() {
46
+ if (IS_GLOBAL) {
47
+ return path.join(HOME, '.claude', 'skills');
48
+ }
55
49
 
56
- fs.mkdirSync(skillsDir, { recursive: true });
50
+ // Walk up from node_modules to find the project root.
51
+ let projectRoot = path.resolve(__dirname, '..');
52
+ while (projectRoot !== path.dirname(projectRoot)) {
53
+ if (path.basename(projectRoot) === 'node_modules') {
54
+ projectRoot = path.dirname(projectRoot);
55
+ break;
56
+ }
57
+ projectRoot = path.dirname(projectRoot);
58
+ }
59
+ return path.join(projectRoot, '.claude', 'skills');
60
+ }
61
+
62
+ function installSharedReferences(skillsDir) {
63
+ const sharedSrc = path.join(SOURCE_DIR, 'shared');
64
+ const sharedDest = path.join(skillsDir, 'shared');
65
+ if (!fs.existsSync(sharedSrc)) return;
57
66
 
58
- // Copy shared references (not a skill, but needed by skills at ../shared/)
59
- const sharedSrc = path.join(SOURCE_DIR, 'shared');
60
- const sharedDest = path.join(skillsDir, 'shared');
61
- if (fs.existsSync(sharedSrc)) {
62
- copyRecursive(sharedSrc, sharedDest);
67
+ // config.json is user-owned: copy everything else, then merge it explicitly.
68
+ const skipConfig = path.join(sharedSrc, CONFIG_FILENAME);
69
+ copyRecursive(sharedSrc, sharedDest, skipConfig);
63
70
  console.log(' 📁 shared references');
71
+
72
+ installSharedConfig(sharedSrc, sharedDest);
64
73
  }
65
74
 
66
- let installed = 0;
67
- for (const skill of skills) {
68
- const src = path.join(SOURCE_DIR, skill);
69
- const dest = path.join(skillsDir, skill);
75
+ function installSharedConfig(sharedSrc, sharedDest) {
76
+ const defaultPath = path.join(sharedSrc, CONFIG_FILENAME);
77
+ const destPath = path.join(sharedDest, CONFIG_FILENAME);
70
78
 
71
- if (fs.existsSync(src)) {
72
- copyRecursive(src, dest);
73
- console.log(` ✅ ${skill}`);
74
- installed++;
75
- } else {
76
- console.log(` ⚠️ ${skill} — source not found, skipping`);
79
+ try {
80
+ const { action } = installConfig(defaultPath, destPath);
81
+ console.log(configLogLine(action));
82
+ } catch (error) {
83
+ console.log(` ⚠️ config install failed (${error.message}) skills still installed`);
77
84
  }
78
85
  }
79
86
 
87
+ function configLogLine(action) {
88
+ switch (action) {
89
+ case 'default':
90
+ return ' 📁 config (default)';
91
+ case 'merged':
92
+ return ' 🔧 config (merged — your settings preserved)';
93
+ case 'recovered':
94
+ return ` ⚠️ config was invalid JSON — backed up to ${CONFIG_FILENAME}.bak, wrote default`;
95
+ default:
96
+ throw new Error(`unknown config action: ${action}`);
97
+ }
98
+ }
99
+
100
+ function installSkills(skillsDir) {
101
+ let installed = 0;
102
+ for (const skill of skills) {
103
+ const src = path.join(SOURCE_DIR, skill);
104
+ const dest = path.join(skillsDir, skill);
105
+
106
+ if (fs.existsSync(src)) {
107
+ copyRecursive(src, dest);
108
+ console.log(` ✅ ${skill}`);
109
+ installed++;
110
+ } else {
111
+ console.log(` ⚠️ ${skill} — source not found, skipping`);
112
+ }
113
+ }
114
+ return installed;
115
+ }
116
+
80
117
  // Copy lib/workflows/ → <skillsDir>/_workflows/ (v2.0.0+).
81
- // Underscore prefix prevents Claude Code from registering as a skill.
82
- if (fs.existsSync(WORKFLOWS_SOURCE_DIR)) {
118
+ // Underscore prefix prevents Claude Code from registering it as a skill.
119
+ function installWorkflows(skillsDir) {
120
+ if (!fs.existsSync(WORKFLOWS_SOURCE_DIR)) return;
83
121
  const workflowsDest = path.join(skillsDir, '_workflows');
84
122
  copyRecursive(WORKFLOWS_SOURCE_DIR, workflowsDest);
85
123
  console.log(' ⚙️ _workflows (lib/workflows + schemas)');
86
124
  }
87
125
 
88
- // Add .claude-scrum-skill to .gitignore if not already present (local install only)
89
- if (!IS_GLOBAL) {
126
+ // Copy lib/guidance/ <skillsDir>/_guidance/ (v2.1.3+).
127
+ // Underscore prefix keeps these internal — orchestrator-injected situational
128
+ // guidance for core epics, never registered as user-facing skills.
129
+ function installGuidance(skillsDir) {
130
+ if (!fs.existsSync(GUIDANCE_SOURCE_DIR)) return;
131
+ const guidanceDest = path.join(skillsDir, '_guidance');
132
+ copyRecursive(GUIDANCE_SOURCE_DIR, guidanceDest);
133
+ console.log(' 🧭 _guidance (design-patterns + domain-modeling)');
134
+ }
135
+
136
+ function ensureGitignoreEntry(skillsDir) {
90
137
  const projectRoot = path.resolve(skillsDir, '..', '..');
91
138
  const gitignorePath = path.join(projectRoot, '.gitignore');
92
139
  const entry = '.claude-scrum-skill';
93
140
 
94
- let needsAppend = true;
95
- if (fs.existsSync(gitignorePath)) {
96
- const contents = fs.readFileSync(gitignorePath, 'utf8');
97
- needsAppend = !contents.split('\n').some(line => line.trim() === entry);
141
+ const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8') : '';
142
+ if (existing.split('\n').some(line => line.trim() === entry)) return;
143
+
144
+ const prefix = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
145
+ fs.appendFileSync(gitignorePath, `${prefix}${entry}\n`);
146
+ console.log(`\n 📝 Added ${entry} to .gitignore`);
147
+ }
148
+
149
+ function copyRecursive(src, dest, skipPath) {
150
+ if (!fs.existsSync(src)) return;
151
+ if (skipPath && path.resolve(src) === path.resolve(skipPath)) return;
152
+
153
+ if (fs.statSync(src).isDirectory()) {
154
+ fs.mkdirSync(dest, { recursive: true });
155
+ for (const item of fs.readdirSync(src)) {
156
+ copyRecursive(path.join(src, item), path.join(dest, item), skipPath);
157
+ }
158
+ } else {
159
+ fs.copyFileSync(src, dest);
98
160
  }
161
+ }
162
+
163
+ function isPlainObject(value) {
164
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
165
+ }
166
+
167
+ // Pure deep-merge: returns a new object. Recurses plain objects; arrays and
168
+ // scalars are leaves with the override winning. Orphan override keys survive.
169
+ function deepMerge(defaults, overrides) {
170
+ const merged = { ...defaults };
99
171
 
100
- if (needsAppend) {
101
- const prefix = fs.existsSync(gitignorePath)
102
- && fs.readFileSync(gitignorePath, 'utf8').length > 0
103
- && !fs.readFileSync(gitignorePath, 'utf8').endsWith('\n')
104
- ? '\n' : '';
105
- fs.appendFileSync(gitignorePath, `${prefix}${entry}\n`);
106
- console.log(`\n 📝 Added ${entry} to .gitignore`);
172
+ for (const key of Object.keys(overrides)) {
173
+ const defaultValue = defaults[key];
174
+ const overrideValue = overrides[key];
175
+
176
+ if (isPlainObject(defaultValue) && isPlainObject(overrideValue)) {
177
+ merged[key] = deepMerge(defaultValue, overrideValue);
178
+ } else {
179
+ merged[key] = overrideValue;
180
+ }
181
+ }
182
+
183
+ return merged;
184
+ }
185
+
186
+ function serializeConfig(config) {
187
+ return `${JSON.stringify(config, null, 2)}\n`;
188
+ }
189
+
190
+ // Three-branch config installer. Returns { action }:
191
+ // 'default' — dest absent, default copied verbatim.
192
+ // 'merged' — dest valid JSON, deep-merged with user values winning.
193
+ // 'recovered' — dest invalid JSON, backed up to .bak, default written.
194
+ function installConfig(defaultPath, destPath) {
195
+ const defaults = JSON.parse(fs.readFileSync(defaultPath, 'utf8'));
196
+
197
+ if (!fs.existsSync(destPath)) {
198
+ fs.writeFileSync(destPath, serializeConfig(defaults));
199
+ return { action: 'default' };
107
200
  }
201
+
202
+ const rawDest = fs.readFileSync(destPath, 'utf8');
203
+ const userConfig = parseConfig(rawDest);
204
+
205
+ if (userConfig === undefined) {
206
+ fs.copyFileSync(destPath, `${destPath}.bak`);
207
+ fs.writeFileSync(destPath, serializeConfig(defaults));
208
+ return { action: 'recovered' };
209
+ }
210
+
211
+ fs.writeFileSync(destPath, serializeConfig(deepMerge(defaults, userConfig)));
212
+ return { action: 'merged' };
213
+ }
214
+
215
+ function parseConfig(raw) {
216
+ try {
217
+ return JSON.parse(raw);
218
+ } catch {
219
+ return undefined;
220
+ }
221
+ }
222
+
223
+ if (require.main === module) {
224
+ main();
108
225
  }
109
226
 
110
- console.log(`\n✨ Installed ${installed} skills to ${skillsDir}`);
111
- console.log(' Restart Claude Code for the skills to become available.\n');
112
- console.log(' Run /project-scaffold <prd-path> to get started.\n');
227
+ module.exports = { deepMerge, installConfig, installSkills, installGuidance };
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@houseofwolvesllc/claude-scrum-skill",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
4
4
  "description": "Claude Code skills for scrum project management — PRD to production release pipeline with project scaffolding, sprint planning, status tracking, sprint releases, full-project emulation testing, autonomous orchestration, and project cleanup.",
5
5
  "bin": {},
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },
9
9
  "scripts": {
10
- "postinstall": "node bin/install.js"
10
+ "postinstall": "node bin/install.js",
11
+ "test": "node --test"
11
12
  },
12
13
  "keywords": [
13
14
  "claude",
@@ -17,7 +17,7 @@ Fully autonomous project lifecycle driver. Plans sprints, executes stories via p
17
17
  2. Read `../shared/config.json` to determine the scaffolding mode (`scaffolding` key: `"local"`, `"github"`, `"jira"`, or `"trello"`, default: `"local"`). If `"local"`, also read the `paths.backlog` and `paths.context` values (`paths.context` defaults to `.claude-scrum-skill/context` and is where Step 3 subagents look for per-epic CONTEXT.md files). Read `../shared/references/PROVIDERS.md` for provider-specific API commands when using a remote provider.
18
18
  3. Read the project's `CLAUDE.md` (if it exists) for project-specific rules. **All subagents you spawn must also read and follow `CLAUDE.md`** — include this instruction explicitly in every subagent prompt.
19
19
  3a. Read `../shared/references/ENGINEERING_BASELINE.md` — the universal engineering baseline (Clean Code, Test-Driven Development, and the simple-design Arbitration Rule). It applies to **all** work this orchestration drives, across every epic and story. Resolve its absolute path; you will pass it to the sprint pipeline as `baselinePath` so every implementation and review subagent follows it. Order of precedence: project `CLAUDE.md` > engineering baseline > situational guidance (`design-patterns`, `domain-modeling`).
20
- 3b. **Subdomain classification gates situational guidance.** `/project-spec` classifies each epic as `core`, `supporting`, or `generic`, and `/project-scaffold` persists that classification into epic metadata: the `subdomain` field of each `_epic.md` frontmatter (local mode) or a `subdomain:<value>` label on the epic's issues (remote modes). Read the classification from that epic metadata — the single authoritative source — and do NOT re-derive it. At implementation time, only **`core`** epics receive the situational guidance skills (`design-patterns`, `domain-modeling`); `supporting`, `generic`, and unclassified epics get the baseline only. Resolve the absolute `SKILL.md` paths for the `design-patterns` and `domain-modeling` skills so you can pass them as `situationalGuidance` for core epics.
20
+ 3b. **Subdomain classification gates situational guidance.** `/project-spec` classifies each epic as `core`, `supporting`, or `generic`, and `/project-scaffold` persists that classification into epic metadata: the `subdomain` field of each `_epic.md` frontmatter (local mode) or a `subdomain:<value>` label on the epic's issues (remote modes). Read the classification from that epic metadata — the single authoritative source — and do NOT re-derive it. At implementation time, only **`core`** epics receive the situational guidance skills (`design-patterns`, `domain-modeling`); `supporting`, `generic`, and unclassified epics get the baseline only. The two guidance files ship in the non-registered `_guidance/` directory — the same mechanism `_workflows/` uses, where the underscore prefix keeps Claude Code from registering them as user-facing skills. Their absolute paths are `<skills-root>/_guidance/design-patterns/SKILL.md` and `<skills-root>/_guidance/domain-modeling/SKILL.md`, where `<skills-root>` is the parent directory of this SKILL.md's parent (for a SKILL.md at `~/.claude/skills/project-orchestrate/SKILL.md`, `<skills-root>` is `~/.claude/skills`). The same algorithm works for global, local, and plugin install layouts. Pass these two paths as `situationalGuidance` for core epics — there is exactly one place to look, no `node_modules` hunting.
21
21
  4. Read `../shared/references/PERSONAS.md` for role preambles. When spawning
22
22
  subagents, select the persona matching each story's `persona:*` label (GitHub mode)
23
23
  or `persona` frontmatter field (local mode). If no persona exists, use `impl` (the default).
@@ -412,7 +412,7 @@ backendMode: local | github | jira | trello
412
412
  repoIdentifier: <owner/repo> (github mode only)
413
413
  personaPreambles: { impl: "...", ops: "...", research: "..." }
414
414
  baselinePath: <absolute path to shared/references/ENGINEERING_BASELINE.md> (always set; applies to every story)
415
- situationalGuidance: <array of absolute SKILL.md paths for design-patterns and domain-modeling — set ONLY when the current epic's subdomain is `core`; omit or pass [] for supporting/generic/untagged epics>
415
+ situationalGuidance: <the two guidance SKILL.md paths from the non-registered _guidance/ directory — `<skills-root>/_guidance/design-patterns/SKILL.md` and `<skills-root>/_guidance/domain-modeling/SKILL.md` (resolve `<skills-root>` per Step 3b) — set ONLY when the current epic's subdomain is `core`; omit or pass [] for supporting/generic/untagged epics>
416
416
  ```
417
417
 
418
418
  Wait for the workflow to return. The return is `SprintStoryReturn[]` — one entry per completed (or blocked / failed) story per `lib/workflows/schemas/SprintStoryReturnSchema.json`.