forgedev 1.2.0 → 1.3.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/README.md +57 -10
- package/bin/chainproof.js +126 -0
- package/package.json +25 -7
- package/src/chainproof-bridge.js +330 -0
- package/src/ci-mode.js +85 -0
- package/src/claude-configurator.js +86 -49
- package/src/cli.js +30 -7
- package/src/composer.js +159 -34
- package/src/doctor-checks-chainproof.js +106 -0
- package/src/doctor-checks.js +39 -20
- package/src/doctor-prompts.js +9 -9
- package/src/doctor.js +37 -4
- package/src/guided.js +3 -3
- package/src/index.js +31 -10
- package/src/init-mode.js +64 -11
- package/src/menu.js +178 -0
- package/src/prompts.js +5 -12
- package/src/recommender.js +134 -10
- package/src/scanner.js +57 -2
- package/src/uat-generator.js +204 -189
- package/src/update-check.js +9 -4
- package/src/update.js +1 -1
- package/src/utils.js +64 -5
- package/templates/ai/guardrails-py/backend/app/ai/__init__.py +29 -0
- package/templates/ai/guardrails-py/backend/app/ai/audit_log.py +133 -0
- package/templates/ai/guardrails-py/backend/app/ai/client.py.template +323 -0
- package/templates/ai/guardrails-py/backend/app/ai/health.py.template +157 -0
- package/templates/ai/guardrails-py/backend/app/ai/input_guard.py +98 -0
- package/templates/ai/guardrails-ts/src/lib/ai/audit-log.ts.template +164 -0
- package/templates/ai/guardrails-ts/src/lib/ai/client.ts.template +403 -0
- package/templates/ai/guardrails-ts/src/lib/ai/health.ts.template +165 -0
- package/templates/ai/guardrails-ts/src/lib/ai/index.ts.template +17 -0
- package/templates/ai/guardrails-ts/src/lib/ai/input-guard.ts.template +124 -0
- package/templates/auth/nextauth/src/lib/auth.ts.template +12 -7
- package/templates/backend/express/Dockerfile.template +18 -0
- package/templates/backend/express/package.json.template +33 -0
- package/templates/backend/express/src/index.ts.template +34 -0
- package/templates/backend/express/src/routes/health.ts.template +27 -0
- package/templates/backend/express/tsconfig.json +17 -0
- package/templates/backend/fastapi/backend/Dockerfile.template +5 -0
- package/templates/backend/fastapi/backend/app/api/health.py.template +1 -1
- package/templates/backend/fastapi/backend/app/core/config.py.template +1 -1
- package/templates/backend/fastapi/backend/app/core/errors.py +1 -1
- package/templates/backend/fastapi/backend/app/main.py.template +3 -1
- package/templates/backend/fastapi/backend/requirements.txt.template +2 -0
- package/templates/backend/hono/Dockerfile.template +18 -0
- package/templates/backend/hono/package.json.template +31 -0
- package/templates/backend/hono/src/index.ts.template +32 -0
- package/templates/backend/hono/src/routes/health.ts.template +27 -0
- package/templates/backend/hono/tsconfig.json +18 -0
- package/templates/base/docs/uat/UAT_TEMPLATE.md.template +1 -1
- package/templates/chainproof/base/.chainproof/config.json.template +11 -0
- package/templates/chainproof/base/.chainproof/mcp-server.mjs +310 -0
- package/templates/chainproof/base/.mcp.json +9 -0
- package/templates/chainproof/fastapi/.chainproof/middleware.json.template +14 -0
- package/templates/chainproof/nextjs/.chainproof/hooks.json.template +19 -0
- package/templates/chainproof/polyglot/.chainproof/config.json.template +21 -0
- package/templates/claude-code/agents/architect.md +25 -11
- package/templates/claude-code/agents/build-error-resolver.md +19 -5
- package/templates/claude-code/agents/chief-of-staff.md +42 -8
- package/templates/claude-code/agents/code-quality-reviewer.md +14 -0
- package/templates/claude-code/agents/database-reviewer.md +15 -1
- package/templates/claude-code/agents/deep-reviewer.md +191 -0
- package/templates/claude-code/agents/doc-updater.md +19 -5
- package/templates/claude-code/agents/docs-lookup.md +19 -5
- package/templates/claude-code/agents/e2e-runner.md +26 -12
- package/templates/claude-code/agents/enforcement-gate.md +102 -0
- package/templates/claude-code/agents/frontend-builder.md +188 -0
- package/templates/claude-code/agents/harness-optimizer.md +36 -1
- package/templates/claude-code/agents/loop-operator.md +27 -13
- package/templates/claude-code/agents/planner.md +21 -7
- package/templates/claude-code/agents/product-strategist.md +24 -10
- package/templates/claude-code/agents/production-readiness.md +14 -0
- package/templates/claude-code/agents/prompt-auditor.md +115 -0
- package/templates/claude-code/agents/refactor-cleaner.md +22 -8
- package/templates/claude-code/agents/security-reviewer.md +14 -0
- package/templates/claude-code/agents/spec-validator.md +15 -1
- package/templates/claude-code/agents/tdd-guide.md +21 -7
- package/templates/claude-code/agents/uat-validator.md +14 -0
- package/templates/claude-code/claude-md/base.md +14 -7
- package/templates/claude-code/claude-md/fastapi.md +8 -8
- package/templates/claude-code/claude-md/fullstack.md +6 -6
- package/templates/claude-code/claude-md/hono.md +18 -0
- package/templates/claude-code/claude-md/nextjs.md +5 -5
- package/templates/claude-code/claude-md/remix.md +18 -0
- package/templates/claude-code/commands/audit-security.md +14 -0
- package/templates/claude-code/commands/audit-spec.md +14 -0
- package/templates/claude-code/commands/audit-wiring.md +14 -0
- package/templates/claude-code/commands/build-fix.md +28 -0
- package/templates/claude-code/commands/build-ui.md +59 -0
- package/templates/claude-code/commands/code-review.md +53 -31
- package/templates/claude-code/commands/fix-loop.md +211 -0
- package/templates/claude-code/commands/full-audit.md +36 -8
- package/templates/claude-code/commands/generate-prd.md +1 -1
- package/templates/claude-code/commands/generate-sdd.md +74 -0
- package/templates/claude-code/commands/generate-uat.md +107 -35
- package/templates/claude-code/commands/help.md +68 -0
- package/templates/claude-code/commands/live-uat.md +268 -0
- package/templates/claude-code/commands/optimize-claude-md.md +15 -1
- package/templates/claude-code/commands/plan.md +3 -3
- package/templates/claude-code/commands/pre-pr.md +57 -19
- package/templates/claude-code/commands/product-strategist.md +21 -0
- package/templates/claude-code/commands/resume-session.md +10 -10
- package/templates/claude-code/commands/run-uat.md +59 -2
- package/templates/claude-code/commands/save-session.md +10 -10
- package/templates/claude-code/commands/simplify.md +36 -0
- package/templates/claude-code/commands/tdd.md +17 -18
- package/templates/claude-code/commands/verify-all.md +24 -0
- package/templates/claude-code/commands/verify-intent.md +55 -0
- package/templates/claude-code/commands/workflows.md +52 -40
- package/templates/claude-code/hooks/polyglot.json +10 -1
- package/templates/claude-code/hooks/python.json +10 -1
- package/templates/claude-code/hooks/scripts/autofix-polyglot.mjs +2 -2
- package/templates/claude-code/hooks/scripts/autofix-python.mjs +1 -1
- package/templates/claude-code/hooks/scripts/autofix-typescript.mjs +1 -1
- package/templates/claude-code/hooks/scripts/code-hygiene.mjs +293 -0
- package/templates/claude-code/hooks/scripts/pre-commit-gate.mjs +207 -0
- package/templates/claude-code/hooks/typescript.json +10 -1
- package/templates/claude-code/skills/ai-prompts/SKILL.md +119 -41
- package/templates/claude-code/skills/git-workflow/SKILL.md +5 -5
- package/templates/claude-code/skills/nextjs/SKILL.md +1 -1
- package/templates/claude-code/skills/playwright/SKILL.md +5 -5
- package/templates/claude-code/skills/security-api/SKILL.md +1 -1
- package/templates/claude-code/skills/security-web/SKILL.md +1 -1
- package/templates/claude-code/skills/testing-patterns/SKILL.md +9 -9
- package/templates/database/prisma-postgres/{.env.example → .env.example.template} +1 -0
- package/templates/database/sqlalchemy-postgres/{.env.example → .env.example.template} +1 -0
- package/templates/docs-portal/fastapi/backend/app/portal/__pycache__/docs_reader.cpython-314.pyc +0 -0
- package/templates/docs-portal/fastapi/backend/app/portal/docs_reader.py +201 -0
- package/templates/docs-portal/fastapi/backend/app/portal/html_renderer.py +229 -0
- package/templates/docs-portal/fastapi/backend/app/portal/router.py.template +35 -0
- package/templates/docs-portal/nextjs/src/app/portal/[category]/[slug]/page.tsx +81 -0
- package/templates/docs-portal/nextjs/src/app/portal/[category]/page.tsx +65 -0
- package/templates/docs-portal/nextjs/src/app/portal/layout.tsx.template +54 -0
- package/templates/docs-portal/nextjs/src/app/portal/page.tsx +85 -0
- package/templates/docs-portal/nextjs/src/components/portal/markdown-renderer.tsx +101 -0
- package/templates/docs-portal/nextjs/src/components/portal/mobile-portal-nav.tsx +81 -0
- package/templates/docs-portal/nextjs/src/components/portal/portal-nav.tsx +86 -0
- package/templates/docs-portal/nextjs/src/lib/docs.ts +139 -0
- package/templates/frontend/nextjs/package.json.template +3 -1
- package/templates/frontend/react/index.html.template +12 -0
- package/templates/frontend/react/package.json.template +34 -0
- package/templates/frontend/react/src/App.tsx.template +10 -0
- package/templates/frontend/react/src/index.css +1 -0
- package/templates/frontend/react/src/main.tsx +10 -0
- package/templates/frontend/react/tsconfig.json +17 -0
- package/templates/frontend/react/vite.config.ts.template +15 -0
- package/templates/frontend/react/vitest.config.ts +9 -0
- package/templates/frontend/remix/app/root.tsx.template +31 -0
- package/templates/frontend/remix/app/routes/_index.tsx.template +19 -0
- package/templates/frontend/remix/app/routes/api.health.ts.template +10 -0
- package/templates/frontend/remix/app/tailwind.css +1 -0
- package/templates/frontend/remix/package.json.template +39 -0
- package/templates/frontend/remix/tsconfig.json +18 -0
- package/templates/frontend/remix/vite.config.ts.template +7 -0
- package/templates/infra/github-actions/.github/workflows/ci.yml.template +3 -0
- package/docs/00-README.md +0 -310
- package/docs/01-universal-prompt-library.md +0 -1049
- package/docs/02-claude-code-mastery-playbook.md +0 -283
- package/docs/03-multi-agent-verification.md +0 -565
- package/docs/04-errata-and-verification-checklist.md +0 -284
- package/docs/05-universal-scaffolder-vision.md +0 -452
- package/docs/06-confidence-assessment-and-repo-prompt.md +0 -407
- package/docs/errata.md +0 -58
- package/docs/multi-agent-verification.md +0 -66
- package/docs/playbook.md +0 -95
- package/docs/prompt-library.md +0 -160
- package/docs/uat/UAT_CHECKLIST.csv +0 -9
- package/docs/uat/UAT_TEMPLATE.md +0 -163
- package/templates/claude-code/commands/done.md +0 -19
- /package/{docs/plans/.gitkeep → templates/docs-portal/fastapi/backend/app/portal/__init__.py} +0 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChainProof bridge - pure Node.js trust chain operations.
|
|
3
|
+
* Uses node:crypto for Ed25519 + SHA-256, no Python dependency needed.
|
|
4
|
+
* All operations are file-based, reading and writing to .chainproof/.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { createHash, generateKeyPairSync, sign, verify, randomUUID } from 'node:crypto';
|
|
10
|
+
|
|
11
|
+
// --- Crypto primitives (interoperable with chainproof/core/crypto.py) ---
|
|
12
|
+
|
|
13
|
+
export function hashContent(content) {
|
|
14
|
+
return createHash('sha256').update(content, 'utf-8').digest('hex');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildSignablePayload(entry) {
|
|
18
|
+
// Length-prefixed using UTF-8 byte length for cross-language interop.
|
|
19
|
+
// JS .length and Python len() diverge on emoji; Buffer.byteLength is stable.
|
|
20
|
+
const fields = [
|
|
21
|
+
entry.prevHash,
|
|
22
|
+
entry.contentHash,
|
|
23
|
+
entry.timestamp,
|
|
24
|
+
entry.entryType,
|
|
25
|
+
entry.sessionId,
|
|
26
|
+
];
|
|
27
|
+
return fields.map((f) => {
|
|
28
|
+
const s = String(f ?? '');
|
|
29
|
+
return `${Buffer.byteLength(s, 'utf-8')}:${s}`;
|
|
30
|
+
}).join(':');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function signEntry(payload, privateKeyPem) {
|
|
34
|
+
const signature = sign(null, Buffer.from(payload, 'utf-8'), privateKeyPem);
|
|
35
|
+
return signature.toString('base64');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function verifySignature(payload, signatureB64, publicKeyPem) {
|
|
39
|
+
try {
|
|
40
|
+
return verify(
|
|
41
|
+
null,
|
|
42
|
+
Buffer.from(payload, 'utf-8'),
|
|
43
|
+
publicKeyPem,
|
|
44
|
+
Buffer.from(signatureB64, 'base64')
|
|
45
|
+
);
|
|
46
|
+
} catch {
|
|
47
|
+
// Invalid key or malformed signature
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function computeChainHash(prevHash, contentHash) {
|
|
53
|
+
return createHash('sha256')
|
|
54
|
+
.update(prevHash + contentHash, 'utf-8')
|
|
55
|
+
.digest('hex');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function generateKeypair() {
|
|
59
|
+
const { publicKey, privateKey } = generateKeyPairSync('ed25519', {
|
|
60
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
61
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
62
|
+
});
|
|
63
|
+
return { publicKey, privateKey };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// --- High-level chain operations ---
|
|
67
|
+
|
|
68
|
+
const GENESIS_HASH = '0'.repeat(64);
|
|
69
|
+
|
|
70
|
+
export function initChainproof(projectDir) {
|
|
71
|
+
const cpDir = path.join(projectDir, '.chainproof');
|
|
72
|
+
const keysDir = path.join(cpDir, 'keys');
|
|
73
|
+
|
|
74
|
+
fs.mkdirSync(cpDir, { recursive: true });
|
|
75
|
+
fs.mkdirSync(keysDir, { recursive: true });
|
|
76
|
+
|
|
77
|
+
// Config — merge with any template-composed config (from templates/chainproof/)
|
|
78
|
+
const configPath = path.join(cpDir, 'config.json');
|
|
79
|
+
const projectName = path.basename(projectDir);
|
|
80
|
+
const defaults = {
|
|
81
|
+
version: '1.0.0',
|
|
82
|
+
projectName,
|
|
83
|
+
hashAlgorithm: 'sha256',
|
|
84
|
+
signatureAlgorithm: 'ed25519',
|
|
85
|
+
createdAt: new Date().toISOString(),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (fs.existsSync(configPath)) {
|
|
89
|
+
// Template already composed a config — merge our defaults under it
|
|
90
|
+
try {
|
|
91
|
+
const existing = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
92
|
+
const merged = { ...defaults, ...existing, createdAt: existing.createdAt || defaults.createdAt };
|
|
93
|
+
fs.writeFileSync(configPath, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
|
|
94
|
+
} catch {
|
|
95
|
+
// Corrupted config, overwrite with defaults
|
|
96
|
+
fs.writeFileSync(configPath, JSON.stringify(defaults, null, 2) + '\n', 'utf-8');
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
fs.writeFileSync(configPath, JSON.stringify(defaults, null, 2) + '\n', 'utf-8');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Chain — only create if missing
|
|
103
|
+
const chainPath = path.join(cpDir, 'chain.json');
|
|
104
|
+
if (!fs.existsSync(chainPath)) {
|
|
105
|
+
fs.writeFileSync(
|
|
106
|
+
chainPath,
|
|
107
|
+
JSON.stringify({ entries: [], currentHash: GENESIS_HASH }, null, 2) + '\n',
|
|
108
|
+
'utf-8'
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Artifacts — only create if missing
|
|
113
|
+
const artifactsPath = path.join(cpDir, 'artifacts.json');
|
|
114
|
+
if (!fs.existsSync(artifactsPath)) {
|
|
115
|
+
fs.writeFileSync(
|
|
116
|
+
artifactsPath,
|
|
117
|
+
JSON.stringify({ artifacts: [] }, null, 2) + '\n',
|
|
118
|
+
'utf-8'
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Generate keypair only if missing
|
|
123
|
+
const privateKeyPath = path.join(keysDir, 'private.pem');
|
|
124
|
+
const publicKeyPath = path.join(keysDir, 'public.pem');
|
|
125
|
+
if (!fs.existsSync(privateKeyPath) || !fs.existsSync(publicKeyPath)) {
|
|
126
|
+
const { publicKey, privateKey } = generateKeypair();
|
|
127
|
+
fs.writeFileSync(privateKeyPath, privateKey, { encoding: 'utf-8', mode: 0o600 });
|
|
128
|
+
fs.writeFileSync(publicKeyPath, publicKey, 'utf-8');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Gitignore for keys
|
|
132
|
+
const gitignorePath = path.join(cpDir, '.gitignore');
|
|
133
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
134
|
+
fs.writeFileSync(gitignorePath, 'keys/\n', 'utf-8');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function recordDecision(projectDir, entry) {
|
|
139
|
+
if (!entry || typeof entry.content !== 'string') {
|
|
140
|
+
throw new Error('entry.content must be a non-empty string');
|
|
141
|
+
}
|
|
142
|
+
if (entry.content.length > 1_048_576) {
|
|
143
|
+
throw new Error('entry.content exceeds 1MB limit');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const cpDir = path.join(projectDir, '.chainproof');
|
|
147
|
+
const chainPath = path.join(cpDir, 'chain.json');
|
|
148
|
+
|
|
149
|
+
let chain;
|
|
150
|
+
try {
|
|
151
|
+
chain = JSON.parse(fs.readFileSync(chainPath, 'utf-8'));
|
|
152
|
+
} catch (err) {
|
|
153
|
+
throw new Error(`Failed to read chain.json: ${err.message}`, { cause: err });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const contentHash = hashContent(entry.content);
|
|
157
|
+
const prevHash = chain.currentHash;
|
|
158
|
+
const chainHash = computeChainHash(prevHash, contentHash);
|
|
159
|
+
const timestamp = new Date().toISOString();
|
|
160
|
+
const entryType = entry.entryType || 'decision';
|
|
161
|
+
const sessionId = entry.sessionId || 'default';
|
|
162
|
+
|
|
163
|
+
// Build the full entry before signing so all metadata is covered
|
|
164
|
+
const nllEntry = {
|
|
165
|
+
id: randomUUID(),
|
|
166
|
+
timestamp,
|
|
167
|
+
entryType,
|
|
168
|
+
content: entry.content,
|
|
169
|
+
contentHash,
|
|
170
|
+
prevHash,
|
|
171
|
+
chainHash,
|
|
172
|
+
signature: null,
|
|
173
|
+
sessionId,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Sign the full canonical payload (not just content)
|
|
177
|
+
const privateKeyPath = path.join(cpDir, 'keys', 'private.pem');
|
|
178
|
+
if (fs.existsSync(privateKeyPath)) {
|
|
179
|
+
const privateKey = fs.readFileSync(privateKeyPath, 'utf-8');
|
|
180
|
+
const payload = buildSignablePayload(nllEntry);
|
|
181
|
+
nllEntry.signature = signEntry(payload, privateKey);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
chain.entries.push(nllEntry);
|
|
185
|
+
chain.currentHash = chainHash;
|
|
186
|
+
|
|
187
|
+
// Atomic write: write to temp file first, then rename
|
|
188
|
+
const tmpPath = chainPath + '.tmp';
|
|
189
|
+
fs.writeFileSync(tmpPath, JSON.stringify(chain, null, 2) + '\n', 'utf-8');
|
|
190
|
+
fs.renameSync(tmpPath, chainPath);
|
|
191
|
+
|
|
192
|
+
return nllEntry;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function recordCodeArtifact(projectDir, artifact) {
|
|
196
|
+
const cpDir = path.join(projectDir, '.chainproof');
|
|
197
|
+
const artifactsPath = path.join(cpDir, 'artifacts.json');
|
|
198
|
+
|
|
199
|
+
let data;
|
|
200
|
+
try {
|
|
201
|
+
data = JSON.parse(fs.readFileSync(artifactsPath, 'utf-8'));
|
|
202
|
+
} catch (err) {
|
|
203
|
+
throw new Error(`Failed to read artifacts.json: ${err.message}`, { cause: err });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const record = {
|
|
207
|
+
id: randomUUID(),
|
|
208
|
+
timestamp: new Date().toISOString(),
|
|
209
|
+
filePath: artifact.filePath,
|
|
210
|
+
contentHash: artifact.contentHash || hashContent(artifact.content || ''),
|
|
211
|
+
language: artifact.language || null,
|
|
212
|
+
generator: artifact.generator || null,
|
|
213
|
+
promptHash: artifact.promptHash || null,
|
|
214
|
+
nllEntryId: artifact.nllEntryId || null,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
data.artifacts.push(record);
|
|
218
|
+
|
|
219
|
+
// Atomic write: write to temp file first, then rename
|
|
220
|
+
const tmpPath = artifactsPath + '.tmp';
|
|
221
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
222
|
+
fs.renameSync(tmpPath, artifactsPath);
|
|
223
|
+
|
|
224
|
+
return record;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function verifyChain(projectDir) {
|
|
228
|
+
const cpDir = path.join(projectDir, '.chainproof');
|
|
229
|
+
const chainPath = path.join(cpDir, 'chain.json');
|
|
230
|
+
|
|
231
|
+
if (!fs.existsSync(chainPath)) {
|
|
232
|
+
return { valid: false, errors: ['chain.json not found'] };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const chain = JSON.parse(fs.readFileSync(chainPath, 'utf-8'));
|
|
236
|
+
const errors = [];
|
|
237
|
+
let expectedHash = GENESIS_HASH;
|
|
238
|
+
|
|
239
|
+
for (let i = 0; i < chain.entries.length; i++) {
|
|
240
|
+
const entry = chain.entries[i];
|
|
241
|
+
|
|
242
|
+
// Verify prev_hash links correctly
|
|
243
|
+
if (entry.prevHash !== expectedHash) {
|
|
244
|
+
errors.push(
|
|
245
|
+
`Entry ${i}: prevHash mismatch (expected ${expectedHash.slice(0, 8)}..., got ${entry.prevHash.slice(0, 8)}...)`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Verify content hash
|
|
250
|
+
const actualContentHash = hashContent(entry.content);
|
|
251
|
+
if (entry.contentHash !== actualContentHash) {
|
|
252
|
+
errors.push(`Entry ${i}: contentHash mismatch (content was tampered)`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Verify chain hash
|
|
256
|
+
const actualChainHash = computeChainHash(entry.prevHash, entry.contentHash);
|
|
257
|
+
if (entry.chainHash !== actualChainHash) {
|
|
258
|
+
errors.push(`Entry ${i}: chainHash mismatch`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Verify signature covers full payload (not just content)
|
|
262
|
+
if (entry.signature) {
|
|
263
|
+
const publicKeyPath = path.join(cpDir, 'keys', 'public.pem');
|
|
264
|
+
if (fs.existsSync(publicKeyPath)) {
|
|
265
|
+
const publicKey = fs.readFileSync(publicKeyPath, 'utf-8');
|
|
266
|
+
const payload = buildSignablePayload(entry);
|
|
267
|
+
if (!verifySignature(payload, entry.signature, publicKey)) {
|
|
268
|
+
errors.push(`Entry ${i}: invalid signature`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
expectedHash = entry.chainHash;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Verify current hash matches last entry
|
|
277
|
+
if (chain.entries.length > 0 && chain.currentHash !== expectedHash) {
|
|
278
|
+
errors.push('currentHash does not match last entry chainHash');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return { valid: errors.length === 0, errors };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function getChainStatus(projectDir) {
|
|
285
|
+
const cpDir = path.join(projectDir, '.chainproof');
|
|
286
|
+
const chainPath = path.join(cpDir, 'chain.json');
|
|
287
|
+
const configPath = path.join(cpDir, 'config.json');
|
|
288
|
+
|
|
289
|
+
if (!fs.existsSync(cpDir)) {
|
|
290
|
+
return { initialized: false };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let chain;
|
|
294
|
+
try {
|
|
295
|
+
chain = JSON.parse(fs.readFileSync(chainPath, 'utf-8'));
|
|
296
|
+
} catch (err) {
|
|
297
|
+
throw new Error(`Failed to read chain.json: ${err.message}`, { cause: err });
|
|
298
|
+
}
|
|
299
|
+
let config = {};
|
|
300
|
+
if (fs.existsSync(configPath)) {
|
|
301
|
+
try {
|
|
302
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
303
|
+
} catch (err) {
|
|
304
|
+
throw new Error(`Failed to read config.json: ${err.message}`, { cause: err });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const artifactsPath = path.join(cpDir, 'artifacts.json');
|
|
309
|
+
let artifacts = { artifacts: [] };
|
|
310
|
+
if (fs.existsSync(artifactsPath)) {
|
|
311
|
+
try {
|
|
312
|
+
artifacts = JSON.parse(fs.readFileSync(artifactsPath, 'utf-8'));
|
|
313
|
+
} catch (err) {
|
|
314
|
+
throw new Error(`Failed to read artifacts.json: ${err.message}`, { cause: err });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const unsignedCount = chain.entries.filter((e) => !e.signature).length;
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
initialized: true,
|
|
322
|
+
projectName: config.projectName || path.basename(projectDir),
|
|
323
|
+
entryCount: chain.entries.length,
|
|
324
|
+
artifactCount: artifacts.artifacts.length,
|
|
325
|
+
currentHash: chain.currentHash,
|
|
326
|
+
unsignedEntries: unsignedCount,
|
|
327
|
+
createdAt: config.createdAt || null,
|
|
328
|
+
lastEntry: chain.entries.length > 0 ? chain.entries[chain.entries.length - 1].timestamp : null,
|
|
329
|
+
};
|
|
330
|
+
}
|
package/src/ci-mode.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { log } from './utils.js';
|
|
5
|
+
import { scanProject, detectStack } from './scanner.js';
|
|
6
|
+
import { runAllChecks } from './doctor-checks.js';
|
|
7
|
+
import { generateReport } from './doctor-prompts.js';
|
|
8
|
+
|
|
9
|
+
export async function runCI(projectDir) {
|
|
10
|
+
const resolvedDir = path.resolve(projectDir);
|
|
11
|
+
|
|
12
|
+
if (!fs.existsSync(resolvedDir)) {
|
|
13
|
+
log.error(`Directory not found: ${resolvedDir}`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
console.log('');
|
|
18
|
+
log.info(' DevForge CI Automated project health check');
|
|
19
|
+
console.log('');
|
|
20
|
+
|
|
21
|
+
// Scan project
|
|
22
|
+
const scan = scanProject(resolvedDir);
|
|
23
|
+
const stack = detectStack(resolvedDir, scan);
|
|
24
|
+
log.dim(` Stack: ${stack || 'unknown'}`);
|
|
25
|
+
|
|
26
|
+
// Run all checks
|
|
27
|
+
const issues = runAllChecks(resolvedDir, scan);
|
|
28
|
+
|
|
29
|
+
const critical = issues.filter(i => i.severity === 'critical');
|
|
30
|
+
const warnings = issues.filter(i => i.severity === 'warning');
|
|
31
|
+
const info = issues.filter(i => i.severity === 'info');
|
|
32
|
+
|
|
33
|
+
// Output results
|
|
34
|
+
console.log('');
|
|
35
|
+
if (critical.length > 0) {
|
|
36
|
+
log.error(` CRITICAL: ${critical.length} issue${critical.length > 1 ? 's' : ''}`);
|
|
37
|
+
for (const issue of critical) {
|
|
38
|
+
console.error(` - ${issue.title}`);
|
|
39
|
+
if (issue.files?.length) {
|
|
40
|
+
for (const detail of issue.files.slice(0, 5)) {
|
|
41
|
+
console.error(chalk.dim(` ${detail}`));
|
|
42
|
+
}
|
|
43
|
+
if (issue.files.length > 5) {
|
|
44
|
+
console.error(chalk.dim(` ... and ${issue.files.length - 5} more`));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (warnings.length > 0) {
|
|
51
|
+
log.warn(` WARNINGS: ${warnings.length} issue${warnings.length > 1 ? 's' : ''}`);
|
|
52
|
+
for (const issue of warnings) {
|
|
53
|
+
console.error(` - ${issue.title}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (info.length > 0) {
|
|
58
|
+
log.dim(` INFO: ${info.length} suggestion${info.length > 1 ? 's' : ''}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (critical.length === 0 && warnings.length === 0) {
|
|
62
|
+
log.success(' All checks passed!');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Save report if docs/ exists
|
|
66
|
+
const docsDir = path.join(resolvedDir, 'docs');
|
|
67
|
+
if (fs.existsSync(docsDir)) {
|
|
68
|
+
const reportPath = path.join(docsDir, 'ci-report.md');
|
|
69
|
+
const projectName = path.basename(resolvedDir);
|
|
70
|
+
const report = generateReport(issues, projectName);
|
|
71
|
+
fs.writeFileSync(reportPath, report, 'utf-8');
|
|
72
|
+
log.dim(` Report saved to docs/ci-report.md`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log('');
|
|
76
|
+
|
|
77
|
+
// Summary line for CI parsers
|
|
78
|
+
const total = issues.length;
|
|
79
|
+
console.log(`DevForge CI: ${critical.length} critical, ${warnings.length} warnings, ${info.length} info (${total} total)`);
|
|
80
|
+
|
|
81
|
+
// Exit with error code if critical issues found
|
|
82
|
+
if (critical.length > 0) {
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { ROOT_DIR, ensureDir, writeFile, readTemplate, replaceVars,
|
|
3
|
+
import { ROOT_DIR, ensureDir, writeFile, readTemplate, replaceVars, getStackCommands } from './utils.js';
|
|
4
4
|
|
|
5
5
|
const CLAUDE_TEMPLATES_DIR = path.join(ROOT_DIR, 'templates', 'claude-code');
|
|
6
6
|
const DOCS_DIR = path.join(ROOT_DIR, 'docs');
|
|
@@ -16,6 +16,15 @@ export async function generateClaudeConfig(outputDir, stackConfig, options = {})
|
|
|
16
16
|
generateAgents(outputDir, stackConfig, vars);
|
|
17
17
|
generateCommands(outputDir, stackConfig, vars);
|
|
18
18
|
copyPromptLibrary(outputDir);
|
|
19
|
+
stampVersion(outputDir);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function stampVersion(outputDir) {
|
|
23
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, 'package.json'), 'utf-8'));
|
|
24
|
+
writeFile(path.join(outputDir, '.claude', '.devforge-version'), JSON.stringify({
|
|
25
|
+
version: pkg.version,
|
|
26
|
+
generatedAt: new Date().toISOString(),
|
|
27
|
+
}, null, 2) + '\n');
|
|
19
28
|
}
|
|
20
29
|
|
|
21
30
|
function buildClaudeVars(config) {
|
|
@@ -24,68 +33,76 @@ function buildClaudeVars(config) {
|
|
|
24
33
|
PROJECT_NAME_PASCAL: config.projectName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(''),
|
|
25
34
|
};
|
|
26
35
|
|
|
36
|
+
// Commands vary by stack (shared with composer)
|
|
37
|
+
Object.assign(vars, getStackCommands(config.stackId));
|
|
38
|
+
|
|
27
39
|
if (config.stackId === 'nextjs-fullstack') {
|
|
28
40
|
vars.STACK_SUMMARY = 'Next.js 15 (App Router) + TypeScript + Tailwind CSS + Prisma + PostgreSQL';
|
|
29
|
-
vars.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
vars.DIR_MAP = `- src/app/ — Next.js App Router pages and API routes
|
|
35
|
-
- src/lib/ — Shared utilities, database client, error handling
|
|
36
|
-
- src/components/ — React components
|
|
37
|
-
- prisma/ — Database schema and migrations
|
|
38
|
-
- e2e/ — Playwright E2E tests`;
|
|
41
|
+
vars.DIR_MAP = `- src/app/ - Next.js App Router pages and API routes
|
|
42
|
+
- src/lib/ - Shared utilities, database client, error handling
|
|
43
|
+
- src/components/ - React components
|
|
44
|
+
- prisma/ - Database schema and migrations
|
|
45
|
+
- e2e/ - Playwright E2E tests`;
|
|
39
46
|
} else if (config.stackId === 'fastapi-backend') {
|
|
40
47
|
vars.STACK_SUMMARY = 'FastAPI + Python + SQLAlchemy 2.0 + PostgreSQL + Alembic';
|
|
41
|
-
vars.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
- backend/
|
|
48
|
-
- backend/
|
|
49
|
-
- backend/app/db/ — Database session and models
|
|
50
|
-
- backend/app/models/ — SQLAlchemy models
|
|
51
|
-
- backend/app/schemas/ — Pydantic schemas
|
|
52
|
-
- backend/tests/ — Pytest tests
|
|
53
|
-
- backend/alembic/ — Database migrations`;
|
|
48
|
+
vars.DIR_MAP = `- backend/app/ - FastAPI application
|
|
49
|
+
- backend/app/api/ - API route handlers
|
|
50
|
+
- backend/app/core/ - Config, security, error handling
|
|
51
|
+
- backend/app/db/ - Database session and models
|
|
52
|
+
- backend/app/models/ - SQLAlchemy models
|
|
53
|
+
- backend/app/schemas/ - Pydantic schemas
|
|
54
|
+
- backend/tests/ - Pytest tests
|
|
55
|
+
- backend/alembic/ - Database migrations`;
|
|
54
56
|
} else if (config.stackId === 'polyglot-fullstack') {
|
|
55
57
|
vars.STACK_SUMMARY = 'Next.js 15 (frontend) + FastAPI (backend) + PostgreSQL';
|
|
56
|
-
vars.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
-
|
|
63
|
-
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
-
|
|
68
|
-
-
|
|
58
|
+
vars.DIR_MAP = `- frontend/ - Next.js 15 App Router application
|
|
59
|
+
- frontend/src/app/ - Pages and API routes
|
|
60
|
+
- frontend/src/lib/ - Shared utilities
|
|
61
|
+
- backend/ - FastAPI application
|
|
62
|
+
- backend/app/api/ - API route handlers
|
|
63
|
+
- backend/app/core/ - Config, security, error handling
|
|
64
|
+
- backend/app/db/ - Database session and models
|
|
65
|
+
- e2e/ - Playwright E2E tests`;
|
|
66
|
+
} else if (config.stackId === 'react-express') {
|
|
67
|
+
vars.STACK_SUMMARY = 'React (Vite) + Express + TypeScript + Prisma + PostgreSQL';
|
|
68
|
+
vars.DIR_MAP = `- frontend/ - React (Vite) SPA
|
|
69
|
+
- frontend/src/ - React components and pages
|
|
70
|
+
- backend/ - Express API server
|
|
71
|
+
- backend/src/ - Express routes and middleware
|
|
72
|
+
- backend/src/routes/ - API route handlers
|
|
73
|
+
- backend/prisma/ - Database schema and migrations`;
|
|
74
|
+
} else if (config.stackId === 'remix-fullstack') {
|
|
75
|
+
vars.STACK_SUMMARY = 'Remix + Vite + TypeScript + Tailwind CSS + Prisma + PostgreSQL';
|
|
76
|
+
vars.DIR_MAP = `- app/ - Remix application
|
|
77
|
+
- app/routes/ - File-based routes and API resource routes
|
|
78
|
+
- app/routes/api.health.ts - Health check endpoint
|
|
79
|
+
- prisma/ - Database schema and migrations`;
|
|
80
|
+
} else if (config.stackId === 'hono-api') {
|
|
81
|
+
vars.STACK_SUMMARY = 'Hono + TypeScript + Prisma + PostgreSQL';
|
|
82
|
+
vars.DIR_MAP = `- src/ - Hono application
|
|
83
|
+
- src/routes/ - API route handlers
|
|
84
|
+
- src/routes/health.ts - Health check endpoints
|
|
85
|
+
- prisma/ - Database schema and migrations`;
|
|
69
86
|
}
|
|
70
87
|
|
|
71
88
|
// Build skills list for CLAUDE.md reference
|
|
72
89
|
const skillsList = [
|
|
73
|
-
'- `@.claude/skills/git-workflow
|
|
74
|
-
'- `@.claude/skills/testing-patterns
|
|
90
|
+
'- `@.claude/skills/git-workflow/`: Git branching, commits, and PR workflow',
|
|
91
|
+
'- `@.claude/skills/testing-patterns/`: Test pyramid, AAA pattern, mocking strategies',
|
|
75
92
|
];
|
|
76
93
|
if (config.frontend?.framework === 'nextjs') {
|
|
77
|
-
skillsList.push('- `@.claude/skills/nextjs
|
|
78
|
-
skillsList.push('- `@.claude/skills/security-web
|
|
94
|
+
skillsList.push('- `@.claude/skills/nextjs/`: Next.js patterns and conventions');
|
|
95
|
+
skillsList.push('- `@.claude/skills/security-web/`: Frontend security practices');
|
|
79
96
|
}
|
|
80
97
|
if (config.backend?.framework === 'fastapi') {
|
|
81
|
-
skillsList.push('- `@.claude/skills/fastapi
|
|
82
|
-
skillsList.push('- `@.claude/skills/security-api
|
|
98
|
+
skillsList.push('- `@.claude/skills/fastapi/`: FastAPI patterns and conventions');
|
|
99
|
+
skillsList.push('- `@.claude/skills/security-api/`: API security practices');
|
|
83
100
|
}
|
|
84
101
|
if (config.testing?.e2e === 'playwright') {
|
|
85
|
-
skillsList.push('- `@.claude/skills/playwright
|
|
102
|
+
skillsList.push('- `@.claude/skills/playwright/`: E2E testing patterns');
|
|
86
103
|
}
|
|
87
104
|
if (config.ai) {
|
|
88
|
-
skillsList.push('- `@.claude/skills/ai-prompts
|
|
105
|
+
skillsList.push('- `@.claude/skills/ai-prompts/`: AI/LLM prompt patterns');
|
|
89
106
|
}
|
|
90
107
|
vars.SKILLS_LIST = skillsList.length > 0 ? skillsList.join('\n') : '- (none generated)';
|
|
91
108
|
|
|
@@ -105,6 +122,12 @@ function generateClaudeMd(outputDir, config, vars) {
|
|
|
105
122
|
stackSection = readTemplate(path.join(CLAUDE_TEMPLATES_DIR, 'claude-md', 'fastapi.md'));
|
|
106
123
|
} else if (config.stackId === 'polyglot-fullstack') {
|
|
107
124
|
stackSection = readTemplate(path.join(CLAUDE_TEMPLATES_DIR, 'claude-md', 'fullstack.md'));
|
|
125
|
+
} else if (config.stackId === 'react-express') {
|
|
126
|
+
stackSection = readTemplate(path.join(CLAUDE_TEMPLATES_DIR, 'claude-md', 'fullstack.md'));
|
|
127
|
+
} else if (config.stackId === 'remix-fullstack') {
|
|
128
|
+
stackSection = readTemplate(path.join(CLAUDE_TEMPLATES_DIR, 'claude-md', 'remix.md'));
|
|
129
|
+
} else if (config.stackId === 'hono-api') {
|
|
130
|
+
stackSection = readTemplate(path.join(CLAUDE_TEMPLATES_DIR, 'claude-md', 'hono.md'));
|
|
108
131
|
}
|
|
109
132
|
|
|
110
133
|
content = content.replace('{{STACK_SPECIFIC_RULES}}', stackSection);
|
|
@@ -115,9 +138,9 @@ function generateClaudeMd(outputDir, config, vars) {
|
|
|
115
138
|
|
|
116
139
|
function generateHooks(outputDir, config, options = {}) {
|
|
117
140
|
let hookFile;
|
|
118
|
-
|
|
141
|
+
const scriptFiles = ['guard-protected-files.mjs', 'code-hygiene.mjs', 'pre-commit-gate.mjs'];
|
|
119
142
|
|
|
120
|
-
if (config.stackId === 'nextjs-fullstack') {
|
|
143
|
+
if (config.stackId === 'nextjs-fullstack' || config.stackId === 'remix-fullstack' || config.stackId === 'hono-api') {
|
|
121
144
|
hookFile = 'typescript.json';
|
|
122
145
|
scriptFiles.push('autofix-typescript.mjs');
|
|
123
146
|
} else if (config.stackId === 'fastapi-backend') {
|
|
@@ -126,6 +149,9 @@ function generateHooks(outputDir, config, options = {}) {
|
|
|
126
149
|
} else if (config.stackId === 'polyglot-fullstack') {
|
|
127
150
|
hookFile = 'polyglot.json';
|
|
128
151
|
scriptFiles.push('autofix-polyglot.mjs');
|
|
152
|
+
} else if (config.stackId === 'react-express') {
|
|
153
|
+
hookFile = 'typescript.json';
|
|
154
|
+
scriptFiles.push('autofix-typescript.mjs');
|
|
129
155
|
}
|
|
130
156
|
|
|
131
157
|
const settingsPath = path.join(outputDir, '.claude', 'settings.json');
|
|
@@ -178,7 +204,7 @@ function deepMergeSettings(existing, incoming) {
|
|
|
178
204
|
}
|
|
179
205
|
|
|
180
206
|
function generateSkills(outputDir, config) {
|
|
181
|
-
// Universal skills
|
|
207
|
+
// Universal skills, always included
|
|
182
208
|
const skillsToInclude = ['git-workflow', 'testing-patterns'];
|
|
183
209
|
|
|
184
210
|
if (config.frontend?.framework === 'nextjs') {
|
|
@@ -227,6 +253,10 @@ function generateAgents(outputDir, config, vars) {
|
|
|
227
253
|
'loop-operator.md',
|
|
228
254
|
'harness-optimizer.md',
|
|
229
255
|
'product-strategist.md',
|
|
256
|
+
'prompt-auditor.md',
|
|
257
|
+
'frontend-builder.md',
|
|
258
|
+
'enforcement-gate.md',
|
|
259
|
+
'deep-reviewer.md',
|
|
230
260
|
];
|
|
231
261
|
|
|
232
262
|
for (const agent of agents) {
|
|
@@ -245,7 +275,6 @@ function generateCommands(outputDir, config, vars) {
|
|
|
245
275
|
'workflows.md',
|
|
246
276
|
'status.md',
|
|
247
277
|
'next.md',
|
|
248
|
-
'done.md',
|
|
249
278
|
'verify-all.md',
|
|
250
279
|
'audit-spec.md',
|
|
251
280
|
'audit-wiring.md',
|
|
@@ -257,11 +286,19 @@ function generateCommands(outputDir, config, vars) {
|
|
|
257
286
|
'optimize-claude-md.md',
|
|
258
287
|
'plan.md',
|
|
259
288
|
'build-fix.md',
|
|
289
|
+
'fix-loop.md',
|
|
260
290
|
'code-review.md',
|
|
261
291
|
'tdd.md',
|
|
262
292
|
'save-session.md',
|
|
263
293
|
'resume-session.md',
|
|
264
294
|
'full-audit.md',
|
|
295
|
+
'verify-intent.md',
|
|
296
|
+
'build-ui.md',
|
|
297
|
+
'help.md',
|
|
298
|
+
'live-uat.md',
|
|
299
|
+
'simplify.md',
|
|
300
|
+
'generate-sdd.md',
|
|
301
|
+
'product-strategist.md',
|
|
265
302
|
];
|
|
266
303
|
|
|
267
304
|
for (const cmd of commands) {
|