enterprise-delivery 0.1.0
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/.claude-plugin/plugin.json +21 -0
- package/.codex-plugin/plugin.json +36 -0
- package/AGENTS.md +73 -0
- package/README.md +127 -0
- package/package.json +28 -0
- package/scripts/enterprise-delivery-validate.mjs +11 -0
- package/skills/add-requirement/SKILL.md +52 -0
- package/skills/analyze-requirement/SKILL.md +48 -0
- package/skills/analyze-tech-stack/SKILL.md +36 -0
- package/skills/change-requirement/SKILL.md +37 -0
- package/skills/complete-task/SKILL.md +46 -0
- package/skills/delivery-lead/SKILL.md +221 -0
- package/skills/delivery-planning/SKILL.md +27 -0
- package/skills/enterprise-kickoff/SKILL.md +25 -0
- package/skills/feature-gate/SKILL.md +22 -0
- package/skills/release-gate/SKILL.md +24 -0
- package/skills/sprint-gate/SKILL.md +23 -0
- package/skills/start-task/SKILL.md +39 -0
- package/skills/story-intake/SKILL.md +23 -0
- package/skills/update-coding-rules/SKILL.md +32 -0
- package/src/cli.mjs +220 -0
- package/src/frontmatter.mjs +121 -0
- package/src/init.mjs +54 -0
- package/src/install-codex.mjs +121 -0
- package/src/markdown.mjs +58 -0
- package/src/repository.mjs +59 -0
- package/src/rules.mjs +814 -0
- package/templates/docs/enterprise/architecture-overview.md +17 -0
- package/templates/docs/enterprise/change-log.md +4 -0
- package/templates/docs/enterprise/changes/CHG-0000-template.md +37 -0
- package/templates/docs/enterprise/current-state.md +48 -0
- package/templates/docs/enterprise/decision-log.md +5 -0
- package/templates/docs/enterprise/product-backlog.md +5 -0
- package/templates/docs/enterprise/project-charter.md +25 -0
- package/templates/docs/enterprise/project-vision.md +61 -0
- package/templates/docs/enterprise/releases/RELEASE-ID/release-notes.md +21 -0
- package/templates/docs/enterprise/releases/RELEASE-ID/release-plan.md +18 -0
- package/templates/docs/enterprise/releases/RELEASE-ID/traceability-matrix.md +5 -0
- package/templates/docs/enterprise/risk-register.md +5 -0
- package/templates/docs/enterprise/roadmap.md +13 -0
- package/templates/docs/enterprise/sprints/SPRINT-ID/sprint-plan.md +21 -0
- package/templates/docs/enterprise/sprints/SPRINT-ID/status-report.md +29 -0
- package/templates/docs/enterprise/stakeholder-register.md +6 -0
- package/templates/docs/enterprise/stories/STORY-0000-template.md +59 -0
- package/templates/docs/enterprise/task-graph.json +24 -0
- package/templates/docs/enterprise/test-strategy.md +15 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export function parseFrontmatter(content) {
|
|
2
|
+
if (!content.startsWith('---\n')) {
|
|
3
|
+
return { data: {}, body: content };
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const endIndex = content.indexOf('\n---\n', 4);
|
|
7
|
+
if (endIndex === -1) {
|
|
8
|
+
throw new Error('Frontmatter starts but does not close.');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const raw = content.slice(4, endIndex);
|
|
12
|
+
const body = content.slice(endIndex + 5);
|
|
13
|
+
return {
|
|
14
|
+
data: parseYamlSubset(raw),
|
|
15
|
+
body
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseYamlSubset(raw) {
|
|
20
|
+
const data = {};
|
|
21
|
+
const lines = raw.split('\n');
|
|
22
|
+
let currentKey = null;
|
|
23
|
+
let currentMapKey = null;
|
|
24
|
+
|
|
25
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
26
|
+
const line = lines[index];
|
|
27
|
+
if (line.trim() === '') {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!line.startsWith(' ')) {
|
|
32
|
+
currentMapKey = null;
|
|
33
|
+
const match = line.match(/^([A-Za-z0-9_-]+):(?:\s*(.*))?$/);
|
|
34
|
+
if (!match) {
|
|
35
|
+
throw new Error(`Unsupported frontmatter line: ${line}`);
|
|
36
|
+
}
|
|
37
|
+
currentKey = match[1];
|
|
38
|
+
const rawValue = match[2] ?? '';
|
|
39
|
+
if (rawValue === '') {
|
|
40
|
+
const nextLine = lines[index + 1] ?? '';
|
|
41
|
+
data[currentKey] = nextLine.startsWith(' - ') ? [] : {};
|
|
42
|
+
} else if (rawValue === '[]') {
|
|
43
|
+
data[currentKey] = [];
|
|
44
|
+
} else {
|
|
45
|
+
data[currentKey] = unquote(rawValue);
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!currentKey) {
|
|
51
|
+
throw new Error(`Indented frontmatter value has no parent: ${line}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (line.startsWith(' - ')) {
|
|
55
|
+
if (!Array.isArray(data[currentKey])) {
|
|
56
|
+
throw new Error(`Frontmatter key is not an array: ${currentKey}`);
|
|
57
|
+
}
|
|
58
|
+
const value = line.slice(4).trim();
|
|
59
|
+
if (/^[A-Za-z0-9_-]+:\s*.*$/.test(value)) {
|
|
60
|
+
throw new Error(`Unsupported array item: ${value}`);
|
|
61
|
+
}
|
|
62
|
+
data[currentKey].push(unquote(value));
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (line.startsWith(' - ') && currentMapKey) {
|
|
67
|
+
if (!isPlainMap(data[currentKey]) || !Array.isArray(data[currentKey][currentMapKey])) {
|
|
68
|
+
throw new Error(`Frontmatter key is not an array: ${currentKey}.${currentMapKey}`);
|
|
69
|
+
}
|
|
70
|
+
const value = line.slice(6).trim();
|
|
71
|
+
if (/^[A-Za-z0-9_-]+:\s*.*$/.test(value)) {
|
|
72
|
+
throw new Error(`Unsupported array item: ${value}`);
|
|
73
|
+
}
|
|
74
|
+
data[currentKey][currentMapKey].push(unquote(value));
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (line.startsWith(' ')) {
|
|
79
|
+
if (!isPlainMap(data[currentKey])) {
|
|
80
|
+
throw new Error(`Frontmatter key is not a map: ${currentKey}`);
|
|
81
|
+
}
|
|
82
|
+
const mapMatch = line.match(/^ ([A-Za-z0-9_-]+):(?:\s*(.*))?$/);
|
|
83
|
+
if (!mapMatch) {
|
|
84
|
+
throw new Error(`Unsupported frontmatter map line: ${line}`);
|
|
85
|
+
}
|
|
86
|
+
const mapKey = mapMatch[1];
|
|
87
|
+
const rawValue = mapMatch[2] ?? '';
|
|
88
|
+
if (rawValue === '') {
|
|
89
|
+
const nextLine = lines[index + 1] ?? '';
|
|
90
|
+
if (nextLine.startsWith(' - ')) {
|
|
91
|
+
data[currentKey][mapKey] = [];
|
|
92
|
+
currentMapKey = mapKey;
|
|
93
|
+
} else {
|
|
94
|
+
throw new Error(`Unsupported nested frontmatter under ${currentKey}.${mapKey}`);
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
data[currentKey][mapKey] = unquote(rawValue);
|
|
98
|
+
currentMapKey = null;
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
throw new Error(`Unsupported nested frontmatter line: ${line}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return data;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isPlainMap(value) {
|
|
110
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function unquote(value) {
|
|
114
|
+
if (value === '""') {
|
|
115
|
+
return '';
|
|
116
|
+
}
|
|
117
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
118
|
+
return value.slice(1, -1);
|
|
119
|
+
}
|
|
120
|
+
return value;
|
|
121
|
+
}
|
package/src/init.mjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const TEMPLATE_ROOT = path.resolve(MODULE_DIR, '..', 'templates', 'docs', 'enterprise');
|
|
7
|
+
|
|
8
|
+
export function initializeEnterprise(rootDir) {
|
|
9
|
+
const created = [];
|
|
10
|
+
const skipped = [];
|
|
11
|
+
|
|
12
|
+
for (const templatePath of listTemplateFiles(TEMPLATE_ROOT)) {
|
|
13
|
+
const relativeTemplatePath = path.relative(TEMPLATE_ROOT, templatePath);
|
|
14
|
+
const targetRelativePath = path.join('docs', 'enterprise', relativeTemplatePath);
|
|
15
|
+
const targetPath = path.join(rootDir, targetRelativePath);
|
|
16
|
+
|
|
17
|
+
if (fs.existsSync(targetPath)) {
|
|
18
|
+
skipped.push(normalizePath(targetRelativePath));
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
23
|
+
fs.copyFileSync(templatePath, targetPath);
|
|
24
|
+
created.push(normalizePath(targetRelativePath));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
command: 'init',
|
|
29
|
+
target: normalizePath(path.resolve(rootDir)),
|
|
30
|
+
status: 'pass',
|
|
31
|
+
created,
|
|
32
|
+
skipped
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function listTemplateFiles(directory) {
|
|
37
|
+
const entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
38
|
+
const files = [];
|
|
39
|
+
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
const entryPath = path.join(directory, entry.name);
|
|
42
|
+
if (entry.isDirectory()) {
|
|
43
|
+
files.push(...listTemplateFiles(entryPath));
|
|
44
|
+
} else if (entry.isFile()) {
|
|
45
|
+
files.push(entryPath);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return files.sort();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizePath(value) {
|
|
53
|
+
return value.split(path.sep).join('/');
|
|
54
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const PACKAGE_ROOT = path.resolve(MODULE_DIR, '..');
|
|
8
|
+
const PLUGIN_NAME = 'enterprise-delivery';
|
|
9
|
+
const MARKETPLACE_ENTRY = {
|
|
10
|
+
name: PLUGIN_NAME,
|
|
11
|
+
source: {
|
|
12
|
+
source: 'local',
|
|
13
|
+
path: './plugins/enterprise-delivery'
|
|
14
|
+
},
|
|
15
|
+
policy: {
|
|
16
|
+
installation: 'AVAILABLE',
|
|
17
|
+
authentication: 'ON_INSTALL'
|
|
18
|
+
},
|
|
19
|
+
category: 'Coding'
|
|
20
|
+
};
|
|
21
|
+
const PAYLOAD_PATHS = [
|
|
22
|
+
'.codex-plugin',
|
|
23
|
+
'skills',
|
|
24
|
+
'scripts',
|
|
25
|
+
'src',
|
|
26
|
+
'templates',
|
|
27
|
+
'README.md',
|
|
28
|
+
'AGENTS.md',
|
|
29
|
+
'package.json'
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export function installCodexPlugin(options = {}) {
|
|
33
|
+
const homeDir = options.homeDir || os.homedir();
|
|
34
|
+
if (!homeDir) {
|
|
35
|
+
throw new Error('Could not determine home directory for Codex plugin installation.');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const marketplaceRoot = path.join(homeDir, '.agents', 'plugins');
|
|
39
|
+
const pluginsRoot = path.join(marketplaceRoot, 'plugins');
|
|
40
|
+
const pluginDir = path.join(pluginsRoot, PLUGIN_NAME);
|
|
41
|
+
const marketplacePath = path.join(marketplaceRoot, 'marketplace.json');
|
|
42
|
+
|
|
43
|
+
fs.mkdirSync(pluginsRoot, { recursive: true });
|
|
44
|
+
fs.rmSync(pluginDir, { recursive: true, force: true });
|
|
45
|
+
fs.mkdirSync(pluginDir, { recursive: true });
|
|
46
|
+
|
|
47
|
+
for (const relativePath of PAYLOAD_PATHS) {
|
|
48
|
+
const sourcePath = path.join(PACKAGE_ROOT, relativePath);
|
|
49
|
+
if (!fs.existsSync(sourcePath)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const targetPath = path.join(pluginDir, relativePath);
|
|
54
|
+
fs.cpSync(sourcePath, targetPath, {
|
|
55
|
+
recursive: true,
|
|
56
|
+
force: true,
|
|
57
|
+
verbatimSymlinks: true
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const marketplace = readMarketplace(marketplacePath);
|
|
62
|
+
marketplace.plugins = marketplace.plugins.filter((plugin) => plugin.name !== PLUGIN_NAME);
|
|
63
|
+
marketplace.plugins.push(MARKETPLACE_ENTRY);
|
|
64
|
+
writeJson(marketplacePath, marketplace);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
command: 'install',
|
|
68
|
+
adapter: 'codex',
|
|
69
|
+
status: 'pass',
|
|
70
|
+
pluginName: PLUGIN_NAME,
|
|
71
|
+
pluginDir: normalizePath(pluginDir),
|
|
72
|
+
marketplacePath: normalizePath(marketplacePath)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function installClaudePlaceholder() {
|
|
77
|
+
return {
|
|
78
|
+
command: 'install',
|
|
79
|
+
adapter: 'claude',
|
|
80
|
+
status: 'pass',
|
|
81
|
+
implemented: false,
|
|
82
|
+
message: 'Claude adapter is planned but not implemented yet.'
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function readMarketplace(marketplacePath) {
|
|
87
|
+
if (!fs.existsSync(marketplacePath)) {
|
|
88
|
+
return {
|
|
89
|
+
name: 'local-marketplace',
|
|
90
|
+
interface: {
|
|
91
|
+
displayName: 'Local Plugins'
|
|
92
|
+
},
|
|
93
|
+
plugins: []
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const marketplace = JSON.parse(fs.readFileSync(marketplacePath, 'utf8'));
|
|
98
|
+
if (!marketplace.name) {
|
|
99
|
+
marketplace.name = 'local-marketplace';
|
|
100
|
+
}
|
|
101
|
+
if (!marketplace.interface || typeof marketplace.interface !== 'object') {
|
|
102
|
+
marketplace.interface = {};
|
|
103
|
+
}
|
|
104
|
+
if (!marketplace.interface.displayName) {
|
|
105
|
+
marketplace.interface.displayName = 'Local Plugins';
|
|
106
|
+
}
|
|
107
|
+
if (!Array.isArray(marketplace.plugins)) {
|
|
108
|
+
marketplace.plugins = [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return marketplace;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function writeJson(filePath, value) {
|
|
115
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
116
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalizePath(value) {
|
|
120
|
+
return value.split(path.sep).join('/');
|
|
121
|
+
}
|
package/src/markdown.mjs
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export function hasSection(markdown, heading) {
|
|
2
|
+
return findSectionStart(markdown, heading) !== -1;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function sectionHasContent(markdown, heading) {
|
|
6
|
+
return getSection(markdown, heading).trim().length > 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getSection(markdown, heading) {
|
|
10
|
+
const lines = markdown.split('\n');
|
|
11
|
+
const start = findSectionStart(markdown, heading);
|
|
12
|
+
|
|
13
|
+
if (start === -1) {
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const collected = [];
|
|
18
|
+
for (let index = start; index < lines.length; index += 1) {
|
|
19
|
+
if (isHeadingLine(lines[index])) {
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
collected.push(lines[index]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return collected.join('\n');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function listIds(markdown, prefix) {
|
|
29
|
+
const expression = new RegExp(`\\b${escapeRegExp(prefix)}-[0-9]{4}\\b`, 'g');
|
|
30
|
+
const matches = markdown.match(expression);
|
|
31
|
+
return Array.from(new Set(matches ?? []));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function containsNoImpactReason(markdown, heading) {
|
|
35
|
+
const section = getSection(markdown, heading).toLowerCase();
|
|
36
|
+
return section.includes('no impact') || section.includes('not applicable');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function findSectionStart(markdown, heading) {
|
|
40
|
+
const lines = markdown.split('\n');
|
|
41
|
+
const headingLine = `## ${heading}`;
|
|
42
|
+
|
|
43
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
44
|
+
if (isHeadingLine(lines[index]) && lines[index].trim() === headingLine) {
|
|
45
|
+
return index + 1;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return -1;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isHeadingLine(line) {
|
|
53
|
+
return line.trim().startsWith('## ');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function escapeRegExp(value) {
|
|
57
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
58
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseFrontmatter } from './frontmatter.mjs';
|
|
4
|
+
|
|
5
|
+
export function findArtifactById(rootDir, folder, id) {
|
|
6
|
+
const directory = path.join(rootDir, 'docs/enterprise', folder);
|
|
7
|
+
if (!fs.existsSync(directory)) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const entries = fs.readdirSync(directory).filter((entry) => entry.endsWith('.md'));
|
|
12
|
+
for (const entry of entries) {
|
|
13
|
+
const absolutePath = path.join(directory, entry);
|
|
14
|
+
const content = fs.readFileSync(absolutePath, 'utf8');
|
|
15
|
+
if (!entry.startsWith(`${id}-`) && entry !== `${id}.md`) {
|
|
16
|
+
let parsed;
|
|
17
|
+
try {
|
|
18
|
+
parsed = parseFrontmatter(content);
|
|
19
|
+
} catch {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (parsed.data.id !== id) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const parsed = parseFrontmatter(content);
|
|
29
|
+
return {
|
|
30
|
+
id,
|
|
31
|
+
path: absolutePath,
|
|
32
|
+
relativePath: path.relative(rootDir, absolutePath),
|
|
33
|
+
content,
|
|
34
|
+
data: parsed.data,
|
|
35
|
+
body: parsed.body
|
|
36
|
+
};
|
|
37
|
+
} catch (parseError) {
|
|
38
|
+
return {
|
|
39
|
+
id,
|
|
40
|
+
path: absolutePath,
|
|
41
|
+
relativePath: path.relative(rootDir, absolutePath),
|
|
42
|
+
content,
|
|
43
|
+
data: {},
|
|
44
|
+
body: '',
|
|
45
|
+
parseError
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function loadStory(rootDir, storyId) {
|
|
54
|
+
return findArtifactById(rootDir, 'stories', storyId);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function loadChange(rootDir, changeId) {
|
|
58
|
+
return findArtifactById(rootDir, 'changes', changeId);
|
|
59
|
+
}
|