refactor(skills): rename _bmad/customizations/ to _bmad/custom/
Shorter, cleaner path for user override files. Updates: - resolver script path lookup + docstring - all 6 agent SKILL.md fallback paths - how-to doc examples - .gitignore (also corrects stale .toml to .yaml) - validator INSTALL_ONLY_PATHS entry Extends ESLint CLI-script overrides to cover src/scripts/** and drops an unused helper from the resolver to satisfy the pre-commit lint gate.
This commit is contained in:
parent
64d3f02615
commit
4a88d321f7
|
|
@ -52,7 +52,7 @@ _bmad
|
||||||
_bmad-output
|
_bmad-output
|
||||||
|
|
||||||
# Personal customization files (team files are committed, personal files are not)
|
# Personal customization files (team files are committed, personal files are not)
|
||||||
_bmad/customizations/*.user.toml
|
_bmad/custom/*.user.yaml
|
||||||
.clinerules
|
.clinerules
|
||||||
# .augment/ is gitignored except tracked config files — add exceptions explicitly
|
# .augment/ is gitignored except tracked config files — add exceptions explicitly
|
||||||
.augment/*
|
.augment/*
|
||||||
|
|
|
||||||
|
|
@ -28,12 +28,12 @@ Every agent skill ships a `customize.yaml` file with its defaults. This file def
|
||||||
### Three-Layer Override Model
|
### Three-Layer Override Model
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Priority 1 (wins): _bmad/customizations/{skill-name}.user.yaml (personal, gitignored)
|
Priority 1 (wins): _bmad/custom/{skill-name}.user.yaml (personal, gitignored)
|
||||||
Priority 2: _bmad/customizations/{skill-name}.yaml (team/org, committed)
|
Priority 2: _bmad/custom/{skill-name}.yaml (team/org, committed)
|
||||||
Priority 3 (last): skill's own customize.yaml (defaults)
|
Priority 3 (last): skill's own customize.yaml (defaults)
|
||||||
```
|
```
|
||||||
|
|
||||||
The `_bmad/customizations/` folder starts empty. Files only appear when someone actively customizes.
|
The `_bmad/custom/` folder starts empty. Files only appear when someone actively customizes.
|
||||||
|
|
||||||
### Merge Rules (per field)
|
### Merge Rules (per field)
|
||||||
|
|
||||||
|
|
@ -64,10 +64,10 @@ This file is the canonical schema. Every field you see is customizable.
|
||||||
|
|
||||||
### 2. Create Your Override File
|
### 2. Create Your Override File
|
||||||
|
|
||||||
Create the `_bmad/customizations/` directory in your project root if it doesn't exist. Then create a file named after the skill:
|
Create the `_bmad/custom/` directory in your project root if it doesn't exist. Then create a file named after the skill:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
_bmad/customizations/
|
_bmad/custom/
|
||||||
bmad-agent-pm.yaml # team overrides (committed to git)
|
bmad-agent-pm.yaml # team overrides (committed to git)
|
||||||
bmad-agent-pm.user.yaml # personal preferences (gitignored)
|
bmad-agent-pm.user.yaml # personal preferences (gitignored)
|
||||||
```
|
```
|
||||||
|
|
@ -83,7 +83,7 @@ Change any combination of name, title, icon, role, identity, communication style
|
||||||
Team override (shallow merge on metadata):
|
Team override (shallow merge on metadata):
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# _bmad/customizations/bmad-agent-pm.yaml
|
# _bmad/custom/bmad-agent-pm.yaml
|
||||||
|
|
||||||
agent:
|
agent:
|
||||||
metadata:
|
metadata:
|
||||||
|
|
@ -133,7 +133,7 @@ Procedural startup steps the agent must execute before presenting its menu:
|
||||||
agent:
|
agent:
|
||||||
critical_actions:
|
critical_actions:
|
||||||
- "Scan {project-root}/docs/compliance/ and load any HIPAA-related documents as context."
|
- "Scan {project-root}/docs/compliance/ and load any HIPAA-related documents as context."
|
||||||
- "Read {project-root}/_bmad/customizations/company-glossary.md if it exists."
|
- "Read {project-root}/_bmad/custom/company-glossary.md if it exists."
|
||||||
```
|
```
|
||||||
|
|
||||||
Critical actions append too. They run top-to-bottom on every activation.
|
Critical actions append too. They run top-to-bottom on every activation.
|
||||||
|
|
@ -154,7 +154,7 @@ agent:
|
||||||
- code: RC
|
- code: RC
|
||||||
description: "Run compliance pre-check"
|
description: "Run compliance pre-check"
|
||||||
prompt: |
|
prompt: |
|
||||||
Read {project-root}/_bmad/customizations/compliance-checklist.md
|
Read {project-root}/_bmad/custom/compliance-checklist.md
|
||||||
and scan all documents in {planning_artifacts} against it.
|
and scan all documents in {planning_artifacts} against it.
|
||||||
Report any gaps and cite the relevant regulatory section.
|
Report any gaps and cite the relevant regulatory section.
|
||||||
```
|
```
|
||||||
|
|
@ -163,7 +163,7 @@ Items not listed in your override keep their defaults.
|
||||||
|
|
||||||
#### Referencing Files
|
#### Referencing Files
|
||||||
|
|
||||||
When a field's text needs to point at a file (in `memories`, `critical_actions`, or a menu item's `prompt`), use a full path rooted at `{project-root}`. Even if the file sits next to your override in `_bmad/customizations/`, spell out the full path: `{project-root}/_bmad/customizations/info.md`. The agent resolves `{project-root}` at runtime.
|
When a field's text needs to point at a file (in `memories`, `critical_actions`, or a menu item's `prompt`), use a full path rooted at `{project-root}`. Even if the file sits next to your override in `_bmad/custom/`, spell out the full path: `{project-root}/_bmad/custom/info.md`. The agent resolves `{project-root}` at runtime.
|
||||||
|
|
||||||
### 4. Personal vs Team
|
### 4. Personal vs Team
|
||||||
|
|
||||||
|
|
@ -172,7 +172,7 @@ When a field's text needs to point at a file (in `memories`, `critical_actions`,
|
||||||
**Personal file** (`bmad-agent-pm.user.yaml`): Gitignored automatically. Use for nickname preferences, tone adjustments, personal workflows.
|
**Personal file** (`bmad-agent-pm.user.yaml`): Gitignored automatically. Use for nickname preferences, tone adjustments, personal workflows.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# _bmad/customizations/bmad-agent-pm.user.yaml
|
# _bmad/custom/bmad-agent-pm.user.yaml
|
||||||
|
|
||||||
agent:
|
agent:
|
||||||
metadata:
|
metadata:
|
||||||
|
|
@ -191,7 +191,7 @@ node {project-root}/_bmad/scripts/resolve-customization.js \
|
||||||
--key agent
|
--key agent
|
||||||
```
|
```
|
||||||
|
|
||||||
`--skill` points at the skill's installed directory (where `customize.yaml` lives). The skill name is derived from the directory's basename, and the script looks up `_bmad/customizations/{skill-name}.yaml` and `{skill-name}.user.yaml` automatically.
|
`--skill` points at the skill's installed directory (where `customize.yaml` lives). The skill name is derived from the directory's basename, and the script looks up `_bmad/custom/{skill-name}.yaml` and `{skill-name}.user.yaml` automatically.
|
||||||
|
|
||||||
Useful invocations:
|
Useful invocations:
|
||||||
|
|
||||||
|
|
@ -221,7 +221,7 @@ Some workflows expose their own customization surface (output paths, review sett
|
||||||
|
|
||||||
**Customization not appearing?**
|
**Customization not appearing?**
|
||||||
|
|
||||||
- Verify your file is in `_bmad/customizations/` with the correct skill name
|
- Verify your file is in `_bmad/custom/` with the correct skill name
|
||||||
- Check YAML indentation (spaces only, no tabs) and make sure block scalars (`|`) are correctly indented
|
- Check YAML indentation (spaces only, no tabs) and make sure block scalars (`|`) are correctly indented
|
||||||
- For agents, customization lives under `agent:` -- keys written below it belong to that key until another top-level key begins
|
- For agents, customization lives under `agent:` -- keys written below it belong to that key until another top-level key begins
|
||||||
- Remember `agent.persona` is replace-wholesale: include every persona field you want, not just the ones you're changing
|
- Remember `agent.persona` is replace-wholesale: include every persona field you want, not just the ones you're changing
|
||||||
|
|
@ -232,4 +232,4 @@ Some workflows expose their own customization surface (output paths, review sett
|
||||||
|
|
||||||
**Need to reset?**
|
**Need to reset?**
|
||||||
|
|
||||||
- Delete your override file from `_bmad/customizations/` -- the skill falls back to its built-in defaults
|
- Delete your override file from `_bmad/custom/` -- the skill falls back to its built-in defaults
|
||||||
|
|
|
||||||
|
|
@ -84,9 +84,9 @@ export default [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// CLI scripts under tools/** and test/**
|
// CLI scripts under tools/**, test/**, and src/scripts/**
|
||||||
{
|
{
|
||||||
files: ['tools/**/*.js', 'tools/**/*.mjs', 'test/**/*.js', 'test/**/*.mjs'],
|
files: ['tools/**/*.js', 'tools/**/*.mjs', 'test/**/*.js', 'test/**/*.mjs', 'src/scripts/**/*.js', 'src/scripts/**/*.mjs'],
|
||||||
rules: {
|
rules: {
|
||||||
// Allow CommonJS patterns for Node CLI scripts
|
// Allow CommonJS patterns for Node CLI scripts
|
||||||
'unicorn/prefer-module': 'off',
|
'unicorn/prefer-module': 'off',
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ description: Strategic business analyst and requirements expert. Use when the us
|
||||||
|
|
||||||
Run: `node {project-root}/_bmad/scripts/resolve-customization.js --skill {skill-root} --key agent`
|
Run: `node {project-root}/_bmad/scripts/resolve-customization.js --skill {skill-root} --key agent`
|
||||||
|
|
||||||
**If the script fails**, resolve the `agent` block yourself from `customize.yaml`, with `{project-root}/_bmad/customizations/{skill-name}.yaml` overriding, and `{skill-name}.user.yaml` overriding both (any missing file is skipped).
|
**If the script fails**, resolve the `agent` block yourself from `customize.yaml`, with `{project-root}/_bmad/custom/{skill-name}.yaml` overriding, and `{skill-name}.user.yaml` overriding both (any missing file is skipped).
|
||||||
|
|
||||||
### Step 2: Adopt Persona
|
### Step 2: Adopt Persona
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ description: Technical documentation specialist and knowledge curator. Use when
|
||||||
|
|
||||||
Run: `node {project-root}/_bmad/scripts/resolve-customization.js --skill {skill-root} --key agent`
|
Run: `node {project-root}/_bmad/scripts/resolve-customization.js --skill {skill-root} --key agent`
|
||||||
|
|
||||||
**If the script fails**, resolve the `agent` block yourself from `customize.yaml`, with `{project-root}/_bmad/customizations/{skill-name}.yaml` overriding, and `{skill-name}.user.yaml` overriding both (any missing file is skipped).
|
**If the script fails**, resolve the `agent` block yourself from `customize.yaml`, with `{project-root}/_bmad/custom/{skill-name}.yaml` overriding, and `{skill-name}.user.yaml` overriding both (any missing file is skipped).
|
||||||
|
|
||||||
### Step 2: Adopt Persona
|
### Step 2: Adopt Persona
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ description: Product manager for PRD creation and requirements discovery. Use wh
|
||||||
|
|
||||||
Run: `node {project-root}/_bmad/scripts/resolve-customization.js --skill {skill-root} --key agent`
|
Run: `node {project-root}/_bmad/scripts/resolve-customization.js --skill {skill-root} --key agent`
|
||||||
|
|
||||||
**If the script fails**, resolve the `agent` block yourself from `customize.yaml`, with `{project-root}/_bmad/customizations/{skill-name}.yaml` overriding, and `{skill-name}.user.yaml` overriding both (any missing file is skipped).
|
**If the script fails**, resolve the `agent` block yourself from `customize.yaml`, with `{project-root}/_bmad/custom/{skill-name}.yaml` overriding, and `{skill-name}.user.yaml` overriding both (any missing file is skipped).
|
||||||
|
|
||||||
### Step 2: Adopt Persona
|
### Step 2: Adopt Persona
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ description: UX designer and UI specialist. Use when the user asks to talk to Sa
|
||||||
|
|
||||||
Run: `node {project-root}/_bmad/scripts/resolve-customization.js --skill {skill-root} --key agent`
|
Run: `node {project-root}/_bmad/scripts/resolve-customization.js --skill {skill-root} --key agent`
|
||||||
|
|
||||||
**If the script fails**, resolve the `agent` block yourself from `customize.yaml`, with `{project-root}/_bmad/customizations/{skill-name}.yaml` overriding, and `{skill-name}.user.yaml` overriding both (any missing file is skipped).
|
**If the script fails**, resolve the `agent` block yourself from `customize.yaml`, with `{project-root}/_bmad/custom/{skill-name}.yaml` overriding, and `{skill-name}.user.yaml` overriding both (any missing file is skipped).
|
||||||
|
|
||||||
### Step 2: Adopt Persona
|
### Step 2: Adopt Persona
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ description: System architect and technical design leader. Use when the user ask
|
||||||
|
|
||||||
Run: `node {project-root}/_bmad/scripts/resolve-customization.js --skill {skill-root} --key agent`
|
Run: `node {project-root}/_bmad/scripts/resolve-customization.js --skill {skill-root} --key agent`
|
||||||
|
|
||||||
**If the script fails**, resolve the `agent` block yourself from `customize.yaml`, with `{project-root}/_bmad/customizations/{skill-name}.yaml` overriding, and `{skill-name}.user.yaml` overriding both (any missing file is skipped).
|
**If the script fails**, resolve the `agent` block yourself from `customize.yaml`, with `{project-root}/_bmad/custom/{skill-name}.yaml` overriding, and `{skill-name}.user.yaml` overriding both (any missing file is skipped).
|
||||||
|
|
||||||
### Step 2: Adopt Persona
|
### Step 2: Adopt Persona
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ description: Senior software engineer for story execution and code implementatio
|
||||||
|
|
||||||
Run: `node {project-root}/_bmad/scripts/resolve-customization.js --skill {skill-root} --key agent`
|
Run: `node {project-root}/_bmad/scripts/resolve-customization.js --skill {skill-root} --key agent`
|
||||||
|
|
||||||
**If the script fails**, resolve the `agent` block yourself from `customize.yaml`, with `{project-root}/_bmad/customizations/{skill-name}.yaml` overriding, and `{skill-name}.user.yaml` overriding both (any missing file is skipped).
|
**If the script fails**, resolve the `agent` block yourself from `customize.yaml`, with `{project-root}/_bmad/custom/{skill-name}.yaml` overriding, and `{skill-name}.user.yaml` overriding both (any missing file is skipped).
|
||||||
|
|
||||||
### Step 2: Adopt Persona
|
### Step 2: Adopt Persona
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
/**
|
/**
|
||||||
* Resolve customization for a BMad skill using three-layer YAML merge.
|
* Resolve customization for a BMad skill using three-layer YAML merge.
|
||||||
*
|
*
|
||||||
* Reads customization from three layers (highest priority first):
|
* Reads customization from three layers (highest priority first):
|
||||||
* 1. {project-root}/_bmad/customizations/{name}.user.yaml (personal, gitignored)
|
* 1. {project-root}/_bmad/custom/{name}.user.yaml (personal, gitignored)
|
||||||
* 2. {project-root}/_bmad/customizations/{name}.yaml (team/org, committed)
|
* 2. {project-root}/_bmad/custom/{name}.yaml (team/org, committed)
|
||||||
* 3. {skill-root}/customize.yaml (skill defaults)
|
* 3. {skill-root}/customize.yaml (skill defaults)
|
||||||
*
|
*
|
||||||
* Skill name is derived from the basename of the skill directory.
|
* Skill name is derived from the basename of the skill directory.
|
||||||
|
|
@ -36,10 +35,7 @@ const yaml = require('yaml');
|
||||||
function findProjectRoot(start) {
|
function findProjectRoot(start) {
|
||||||
let current = path.resolve(start);
|
let current = path.resolve(start);
|
||||||
while (true) {
|
while (true) {
|
||||||
if (
|
if (fs.existsSync(path.join(current, '_bmad')) || fs.existsSync(path.join(current, '.git'))) {
|
||||||
fs.existsSync(path.join(current, '_bmad')) ||
|
|
||||||
fs.existsSync(path.join(current, '.git'))
|
|
||||||
) {
|
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
const parent = path.dirname(current);
|
const parent = path.dirname(current);
|
||||||
|
|
@ -53,9 +49,9 @@ function loadYaml(filePath) {
|
||||||
const raw = fs.readFileSync(filePath, 'utf8');
|
const raw = fs.readFileSync(filePath, 'utf8');
|
||||||
const parsed = yaml.parse(raw);
|
const parsed = yaml.parse(raw);
|
||||||
return parsed && typeof parsed === 'object' ? parsed : {};
|
return parsed && typeof parsed === 'object' ? parsed : {};
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
if (err.code === 'ENOENT') return {};
|
if (error.code === 'ENOENT') return {};
|
||||||
process.stderr.write(`warning: failed to parse ${filePath}: ${err.message}\n`);
|
process.stderr.write(`warning: failed to parse ${filePath}: ${error.message}\n`);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -64,10 +60,6 @@ function isPlainObject(value) {
|
||||||
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMenuArray(value) {
|
|
||||||
return Array.isArray(value) && value.length > 0 && value.every((item) => isPlainObject(item));
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeByKey(base, override, keyName) {
|
function mergeByKey(base, override, keyName) {
|
||||||
const result = [];
|
const result = [];
|
||||||
const indexByKey = new Map();
|
const indexByKey = new Map();
|
||||||
|
|
@ -157,12 +149,8 @@ function mergeAgentBlock(base, override) {
|
||||||
// Merge by `code` when both sides use it; otherwise append.
|
// Merge by `code` when both sides use it; otherwise append.
|
||||||
const baseArr = Array.isArray(baseVal) ? baseVal : [];
|
const baseArr = Array.isArray(baseVal) ? baseVal : [];
|
||||||
const overArr = Array.isArray(overVal) ? overVal : [];
|
const overArr = Array.isArray(overVal) ? overVal : [];
|
||||||
const anyHasCode = [...baseArr, ...overArr].some(
|
const anyHasCode = [...baseArr, ...overArr].some((item) => isPlainObject(item) && item.code !== undefined);
|
||||||
(item) => isPlainObject(item) && item.code !== undefined,
|
mergedAgent[key] = anyHasCode ? mergeByKey(baseArr, overArr, 'code') : appendArrays(baseArr, overArr);
|
||||||
);
|
|
||||||
mergedAgent[key] = anyHasCode
|
|
||||||
? mergeByKey(baseArr, overArr, 'code')
|
|
||||||
: appendArrays(baseArr, overArr);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
|
@ -185,7 +173,7 @@ function extractKey(data, dottedKey) {
|
||||||
if (isPlainObject(current) && part in current) {
|
if (isPlainObject(current) && part in current) {
|
||||||
current = current[part];
|
current = current[part];
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return current;
|
return current;
|
||||||
|
|
@ -195,17 +183,28 @@ function parseArgs(argv) {
|
||||||
const args = { skill: null, keys: [] };
|
const args = { skill: null, keys: [] };
|
||||||
for (let i = 0; i < argv.length; i++) {
|
for (let i = 0; i < argv.length; i++) {
|
||||||
const a = argv[i];
|
const a = argv[i];
|
||||||
if (a === '--skill' || a === '-s') {
|
switch (a) {
|
||||||
|
case '--skill':
|
||||||
|
case '-s': {
|
||||||
args.skill = argv[++i];
|
args.skill = argv[++i];
|
||||||
} else if (a === '--key' || a === '-k') {
|
break;
|
||||||
|
}
|
||||||
|
case '--key':
|
||||||
|
case '-k': {
|
||||||
args.keys.push(argv[++i]);
|
args.keys.push(argv[++i]);
|
||||||
} else if (a === '--help' || a === '-h') {
|
break;
|
||||||
|
}
|
||||||
|
case '--help':
|
||||||
|
case '-h': {
|
||||||
printHelp();
|
printHelp();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
} else {
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
process.stderr.write(`warning: unknown argument: ${a}\n`);
|
process.stderr.write(`warning: unknown argument: ${a}\n`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -241,15 +240,14 @@ function main() {
|
||||||
process.stderr.write(`warning: no defaults found at ${defaultsPath}\n`);
|
process.stderr.write(`warning: no defaults found at ${defaultsPath}\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectRoot =
|
const projectRoot = findProjectRoot(process.cwd()) || findProjectRoot(skillDir);
|
||||||
findProjectRoot(process.cwd()) || findProjectRoot(skillDir);
|
|
||||||
|
|
||||||
let team = {};
|
let team = {};
|
||||||
let user = {};
|
let user = {};
|
||||||
if (projectRoot) {
|
if (projectRoot) {
|
||||||
const customizationsDir = path.join(projectRoot, '_bmad', 'customizations');
|
const customDir = path.join(projectRoot, '_bmad', 'custom');
|
||||||
team = loadYaml(path.join(customizationsDir, `${skillName}.yaml`));
|
team = loadYaml(path.join(customDir, `${skillName}.yaml`));
|
||||||
user = loadYaml(path.join(customizationsDir, `${skillName}.user.yaml`));
|
user = loadYaml(path.join(customDir, `${skillName}.user.yaml`));
|
||||||
}
|
}
|
||||||
|
|
||||||
let merged = mergeAgentBlock(defaults, team);
|
let merged = mergeAgentBlock(defaults, team);
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ function escapeTableCell(str) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path prefixes/patterns that only exist in installed structure, not in source
|
// Path prefixes/patterns that only exist in installed structure, not in source
|
||||||
const INSTALL_ONLY_PATHS = ['_config/', 'customizations/'];
|
const INSTALL_ONLY_PATHS = ['_config/', 'custom/'];
|
||||||
|
|
||||||
// Files that are generated at install time and don't exist in the source tree
|
// Files that are generated at install time and don't exist in the source tree
|
||||||
const INSTALL_GENERATED_FILES = ['config.yaml', 'config.user.yaml'];
|
const INSTALL_GENERATED_FILES = ['config.yaml', 'config.user.yaml'];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue