feat-forge 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +350 -0
  3. package/dist/cli.js +306 -0
  4. package/dist/commands/AbstractCommands.js +16 -0
  5. package/dist/commands/AgentCommands.js +14 -0
  6. package/dist/commands/BranchCommands.js +400 -0
  7. package/dist/commands/CompletionCommands.js +702 -0
  8. package/dist/commands/EnvCommands.js +56 -0
  9. package/dist/commands/FeatureCommands.js +4 -0
  10. package/dist/commands/FixCommands.js +4 -0
  11. package/dist/commands/InitCommands.js +380 -0
  12. package/dist/commands/MaintenanceCommands.js +39 -0
  13. package/dist/commands/ModeCommands.js +15 -0
  14. package/dist/commands/ProxyCommands.js +14 -0
  15. package/dist/commands/ReleaseCommands.js +4 -0
  16. package/dist/commands/ServicesCommands.js +95 -0
  17. package/dist/commands/SubBranchCommands.js +49 -0
  18. package/dist/commands/types/InitOptions.js +1 -0
  19. package/dist/foundation/BranchContext.js +427 -0
  20. package/dist/foundation/ForgeConfig.js +264 -0
  21. package/dist/foundation/ForgeConfigFile.js +391 -0
  22. package/dist/foundation/ForgeContext.js +169 -0
  23. package/dist/foundation/NpmHelper.js +131 -0
  24. package/dist/foundation/PathHelper.js +56 -0
  25. package/dist/foundation/PortAllocator.js +192 -0
  26. package/dist/foundation/Proxy.js +176 -0
  27. package/dist/foundation/Repository.js +431 -0
  28. package/dist/foundation/errors/ForgeError.js +9 -0
  29. package/dist/foundation/errors/_error.config.js +12 -0
  30. package/dist/foundation/errors/generated/ForgeBadStateError.js +11 -0
  31. package/dist/foundation/errors/generated/ForgeConfigError.js +11 -0
  32. package/dist/foundation/errors/generated/ForgeExpectMainRepositoryError.js +11 -0
  33. package/dist/foundation/errors/generated/ForgeModeNotDefinedError.js +11 -0
  34. package/dist/foundation/errors/generated/ForgeNotInActiveBranchError.js +11 -0
  35. package/dist/foundation/errors/generated/ForgePortAllocationsLoadError.js +11 -0
  36. package/dist/foundation/errors/generated/ForgePortNotAssignedError.js +11 -0
  37. package/dist/foundation/errors/generated/ForgePortRangeExhaustedError.js +11 -0
  38. package/dist/foundation/errors/generated/ForgeServicesScanError.js +11 -0
  39. package/dist/foundation/errors/generated/ForgeServicesValidationError.js +11 -0
  40. package/dist/foundation/errors/index.js +13 -0
  41. package/dist/foundation/types/AIAgent.js +1 -0
  42. package/dist/foundation/types/AIAgentName.js +11 -0
  43. package/dist/foundation/types/DeepPartial.js +1 -0
  44. package/dist/foundation/types/IDE.js +1 -0
  45. package/dist/foundation/types/IDEName.js +7 -0
  46. package/dist/foundation/types/ModeConfig.js +1 -0
  47. package/dist/foundation/types/RepositoryInfos.js +1 -0
  48. package/dist/foundation/types/Services.js +156 -0
  49. package/dist/foundation/types/ShellName.js +11 -0
  50. package/dist/lib/agents.js +47 -0
  51. package/dist/lib/bootstrap.js +54 -0
  52. package/dist/lib/branch.js +4 -0
  53. package/dist/lib/config.js +65 -0
  54. package/dist/lib/constants.js +13 -0
  55. package/dist/lib/env.js +20 -0
  56. package/dist/lib/fs.js +156 -0
  57. package/dist/lib/git.js +170 -0
  58. package/dist/lib/hooks.js +98 -0
  59. package/dist/lib/ide.js +75 -0
  60. package/dist/lib/merger.js +103 -0
  61. package/dist/lib/platform.js +13 -0
  62. package/dist/lib/prompt.js +134 -0
  63. package/dist/lib/proxy-dashboard.js +75 -0
  64. package/dist/lib/scanner.js +118 -0
  65. package/dist/lib/services.js +132 -0
  66. package/dist/lib/slug.js +35 -0
  67. package/dist/lib/templates.js +115 -0
  68. package/dist/lib/validator.js +15 -0
  69. package/dist/templates/SPEC.md +21 -0
  70. package/dist/templates/TODO.md +5 -0
  71. package/dist/templates/agent/001.general.Omnibus.agent.md +4 -0
  72. package/dist/templates/agent/002.discovery.Inventorius.agent.md +4 -0
  73. package/dist/templates/agent/003.design.Architecturius.agent.md +8 -0
  74. package/dist/templates/agent/004.plan.Strategos.agent.md +8 -0
  75. package/dist/templates/agent/005.tdd.TestDrivenCodificius.agent.md +8 -0
  76. package/dist/templates/agent/006.code.Codificius.agent.md +8 -0
  77. package/dist/templates/agent/007.simplify.Consolidarius.agent.md +8 -0
  78. package/dist/templates/agent/008.review.Auditorix.agent.md +8 -0
  79. package/dist/templates/agent/009.testwriter.TestScriptor.agent.md +8 -0
  80. package/dist/templates/agent/010.testexecutor.TestExecutor.agent.md +8 -0
  81. package/dist/templates/agent/011.commit.Scribus.agent.md +10 -0
  82. package/dist/templates/agent/CONTEXT.code.md +145 -0
  83. package/dist/templates/agent/CONTEXT.spec.md +98 -0
  84. package/dist/templates/agent/Copilot/Code.agent.md +28 -0
  85. package/dist/templates/agent/Copilot/CodeCommit.agent.md +16 -0
  86. package/dist/templates/agent/Copilot/Feature-Builder.agent.md +49 -0
  87. package/dist/templates/agent/Copilot/Reviewer.agent.md +17 -0
  88. package/dist/templates/agent/Copilot/Simplifier.agent.md +21 -0
  89. package/dist/templates/agent/Copilot/Specs.agent.md +66 -0
  90. package/dist/templates/agent/Copilot/SpecsCommit.agent.md +19 -0
  91. package/dist/templates/agent/Copilot/TODO-Reader.agent.md +18 -0
  92. package/dist/templates/agent/Copilot/Tester.agent.md +12 -0
  93. package/package.json +76 -0
@@ -0,0 +1,118 @@
1
+ import { readdir, stat, access } from 'fs/promises';
2
+ import path from 'path';
3
+ const DEFAULT_MAX_DEPTH = 4;
4
+ const IGNORED_DIRS = new Set(['node_modules', 'dist', '.git']);
5
+ async function hasSubdir(dir, sub) {
6
+ try {
7
+ const p = path.join(dir, sub);
8
+ await access(p);
9
+ const s = await stat(p);
10
+ return s.isDirectory();
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ }
16
+ async function readDirSafe(dir) {
17
+ try {
18
+ return await readdir(dir, { withFileTypes: true });
19
+ }
20
+ catch {
21
+ return [];
22
+ }
23
+ }
24
+ function isIgnored(name) {
25
+ return IGNORED_DIRS.has(name);
26
+ }
27
+ export async function scanGitRepos(rootDir, maxDepth = DEFAULT_MAX_DEPTH) {
28
+ const results = [];
29
+ async function walk(dir, depth) {
30
+ if (depth < 0)
31
+ return;
32
+ const base = path.basename(dir);
33
+ if (isIgnored(base))
34
+ return;
35
+ if (await hasSubdir(dir, '.git')) {
36
+ results.push({ path: path.resolve(dir), name: path.basename(dir), isGit: true });
37
+ return;
38
+ }
39
+ const entries = await readDirSafe(dir);
40
+ for (const e of entries) {
41
+ if (!e.isDirectory())
42
+ continue;
43
+ if (isIgnored(e.name))
44
+ continue;
45
+ try {
46
+ await walk(path.join(dir, e.name), depth - 1);
47
+ }
48
+ catch {
49
+ // ignore errors per-directory
50
+ }
51
+ }
52
+ }
53
+ await walk(path.resolve(rootDir), maxDepth);
54
+ return results;
55
+ }
56
+ export async function findAncestorForgeConfig(startDir) {
57
+ let current = path.resolve(startDir);
58
+ while (true) {
59
+ const candidate = path.join(current, '.feat-forge.json');
60
+ try {
61
+ await access(candidate);
62
+ return candidate;
63
+ }
64
+ catch {
65
+ // not found here
66
+ }
67
+ const parent = path.dirname(current);
68
+ if (parent === current)
69
+ break;
70
+ current = parent;
71
+ }
72
+ return null;
73
+ }
74
+ export async function findFeatureCandidateDirs(rootDir) {
75
+ const results = [];
76
+ const maxDepth = 3;
77
+ async function isCandidate(dir) {
78
+ const name = path.basename(dir).toLowerCase();
79
+ if (name.includes('feature'))
80
+ return true;
81
+ if (await hasSubdir(dir, 'src'))
82
+ return true;
83
+ try {
84
+ await access(path.join(dir, 'package.json'));
85
+ return true;
86
+ }
87
+ catch {
88
+ return false;
89
+ }
90
+ }
91
+ async function walk(dir, depth) {
92
+ if (depth < 0)
93
+ return;
94
+ const base = path.basename(dir);
95
+ if (isIgnored(base))
96
+ return;
97
+ const entries = await readDirSafe(dir);
98
+ try {
99
+ if (await isCandidate(dir)) {
100
+ results.push(path.resolve(dir));
101
+ return;
102
+ }
103
+ }
104
+ catch {
105
+ // ignore
106
+ }
107
+ for (const e of entries) {
108
+ if (!e.isDirectory())
109
+ continue;
110
+ if (isIgnored(e.name))
111
+ continue;
112
+ await walk(path.join(dir, e.name), depth - 1);
113
+ }
114
+ }
115
+ await walk(path.resolve(rootDir), maxDepth);
116
+ return results;
117
+ }
118
+ export default {};
@@ -0,0 +1,132 @@
1
+ import { ForgeServicesScanError } from '../foundation/errors/index.js';
2
+ import { GeneratedServicesDTO, ServicesDTO } from '../foundation/types/Services.js';
3
+ import { FEAT_FORGE_GENERATED_SERVICES_FILE, FEAT_FORGE_SERVICES_FILE } from './constants.js';
4
+ import { pathExists, readJSONFile, writeTextFile } from './fs.js';
5
+ import { slugify } from './slug.js';
6
+ import { ForgeError } from '../foundation/errors/ForgeError.js';
7
+ /**
8
+ * Scan all repositories in a branch for .forge/services.json files
9
+ */
10
+ export async function scanReposServices(branchContext) {
11
+ const repositories = [];
12
+ for (const repo of branchContext.repositories) {
13
+ const servicesFilePath = repo.getRepoConfigPath(FEAT_FORGE_SERVICES_FILE);
14
+ if (!(await pathExists(servicesFilePath))) {
15
+ // Repo has no services file, skip it
16
+ continue;
17
+ }
18
+ try {
19
+ const servicesFile = await readJSONFile(ServicesDTO, servicesFilePath);
20
+ repositories.push({ name: repo.name, services: servicesFile.services });
21
+ }
22
+ catch (error) {
23
+ throw ForgeServicesScanError.extend(error, `Failed to scan services from ${repo.name} at ${servicesFilePath}`);
24
+ }
25
+ }
26
+ return repositories;
27
+ }
28
+ /**
29
+ * Generate and write generated.services.json with port assignments
30
+ */
31
+ export async function generateBranchServicesFile(branchContext, portAllocator) {
32
+ const branchPortAllocation = portAllocator.getAllocation(branchContext.branchName);
33
+ let services = [];
34
+ if (branchPortAllocation) {
35
+ services = branchPortAllocation.getServices();
36
+ }
37
+ const generatedFile = {
38
+ _doNotEdit: "This file is auto-generated by 'forge services scan'. Manual edits will be lost on next generation.",
39
+ generatedAt: new Date(),
40
+ services,
41
+ };
42
+ const filePath = branchContext.getInPath(FEAT_FORGE_GENERATED_SERVICES_FILE);
43
+ const content = JSON.stringify(generatedFile, null, 2);
44
+ await writeTextFile(filePath, content);
45
+ return { path: filePath, services };
46
+ }
47
+ export async function loadGeneratedServicesFile(branchContext) {
48
+ const filePath = branchContext.getInPath(FEAT_FORGE_GENERATED_SERVICES_FILE);
49
+ if (!(await pathExists(filePath))) {
50
+ throw new ForgeError(`❌ Generated services file not found at ${filePath}. Please run 'forge services scan' first.`);
51
+ }
52
+ try {
53
+ return readJSONFile(GeneratedServicesDTO, filePath);
54
+ }
55
+ catch (error) {
56
+ throw ForgeError.extend(error, `❌ Failed to load generated services file at ${filePath}`);
57
+ }
58
+ }
59
+ /**
60
+ * Generate and write .envrc with service environment variables
61
+ */
62
+ export async function generateEnvrcFile(forgeContext, branchContext, services) {
63
+ const branchSlug = branchContext.branchNameSlug;
64
+ const proxyEnabled = forgeContext.options.proxy.enabled;
65
+ const proxyPort = forgeContext.options.proxy.port;
66
+ const varPrefix = forgeContext.options.proxy.environmentVariablePrefix;
67
+ const timestamp = new Date().toISOString();
68
+ const lines = [];
69
+ // Header
70
+ lines.push('# ⚠️ AUTO-GENERATED FILE - DO NOT EDIT MANUALLY');
71
+ lines.push(`# This file was generated by 'forge env update' at ${timestamp}`);
72
+ lines.push("# Any manual edits will be lost on the next 'forge env update' run");
73
+ lines.push('');
74
+ if (forgeContext.options.proxy.envrc_source_up) {
75
+ lines.push('# Load parent .envrc files');
76
+ lines.push('source_up');
77
+ lines.push('');
78
+ }
79
+ // Branch configuration
80
+ lines.push('# --------------------');
81
+ lines.push('# Branch configuration');
82
+ lines.push('# --------------------');
83
+ lines.push(`export ${varPrefix}BRANCH_SLUG="${branchSlug}"`);
84
+ lines.push(`export ${varPrefix}BRANCH_SERVICES_CONFIG="${FEAT_FORGE_GENERATED_SERVICES_FILE}"`);
85
+ lines.push('');
86
+ lines.push('# ----------------------');
87
+ lines.push('# Services configuration');
88
+ lines.push('# ----------------------');
89
+ for (const service of services) {
90
+ const { SLUG, port, url, proxyUrl, path } = getServiceOutputs(forgeContext, branchContext, service);
91
+ const envName = `${varPrefix}${SLUG}`;
92
+ lines.push(`export ${envName}_PORT=${port}`);
93
+ lines.push(`export ${envName}_URL=${url}`);
94
+ lines.push(`${proxyEnabled ? '' : '# '}export ${envName}_PROXY_URL=${proxyUrl}`);
95
+ if (path) {
96
+ lines.push(`export ${envName}_PATH="${path}"`);
97
+ }
98
+ lines.push('');
99
+ }
100
+ // Forge proxy configuration (for future use)
101
+ lines.push('# ------------------------------------------');
102
+ lines.push('# Forge proxy configuration (for future use)');
103
+ lines.push('# ------------------------------------------');
104
+ lines.push(`export ${varPrefix}PROXY_ENABLED=${proxyEnabled}`);
105
+ lines.push(`${proxyEnabled ? '' : '# '}export ${varPrefix}PROXY_PORT=${proxyPort}`);
106
+ lines.push('');
107
+ const content = lines.join('\n');
108
+ // Write to file
109
+ const filePath = branchContext.getInPath(forgeContext.options.proxy.envrc);
110
+ await writeTextFile(filePath, content);
111
+ return { path: filePath, env: content };
112
+ }
113
+ /**
114
+ * Get service output variable values for a service.
115
+ * To be used in .envrc generation or proxy configuration generation.
116
+ */
117
+ export function getServiceOutputs(forgeContext, branchContext, service) {
118
+ const serviceSlug = slugify(service.name, false, true); // Don't allow slashes or hyphens in service slug to keep env var names clean
119
+ const urlSlug = slugify(service.name, false);
120
+ const branchSlug = branchContext.branchNameSlug;
121
+ const proxyPort = forgeContext.options.proxy.port;
122
+ return {
123
+ ...service,
124
+ branchSlug,
125
+ key: `${branchSlug}.${urlSlug}`,
126
+ slug: serviceSlug,
127
+ SLUG: serviceSlug.toUpperCase(),
128
+ urlSlug: urlSlug,
129
+ url: `${service.type}://localhost:${service.port}${service.path || ''}`,
130
+ proxyUrl: `${service.type}://${branchSlug}.${urlSlug}.localhost:${proxyPort}${service.path || ''}`,
131
+ };
132
+ }
@@ -0,0 +1,35 @@
1
+ import { promptConfirm } from './prompt.js';
2
+ /**
3
+ * Slugify a user-provided input to be filesystem and git-branch safe.
4
+ */
5
+ export function slugify(input, allowSlash = false, hyphenToUnderscore = false) {
6
+ const trimmed = input.trim();
7
+ let lowered = trimmed.toLowerCase();
8
+ let allowedChars = allowSlash ? 'a-z0-9/_-' : 'a-z0-9_-';
9
+ if (hyphenToUnderscore) {
10
+ lowered = lowered.replace(/-/g, '_');
11
+ }
12
+ const replaceRegex = new RegExp(`[^${allowedChars}]+`, 'g');
13
+ const replaced = lowered.replace(replaceRegex, '-');
14
+ const collapsed = replaced.replace(/-+/g, '-');
15
+ const cleaned = collapsed.replace(/^[.-]+/, '').replace(/[-.]+$/, '');
16
+ return cleaned;
17
+ }
18
+ /**
19
+ * Confirm a slugified input with the user if it differs.
20
+ * Slash are allowed in branch names, but not in feature slugs, so we allow them optionally in the slugification step and only ask for confirmation if the slugified result differs from the input.
21
+ */
22
+ export async function confirmSlugOrThrow(input, allowSlash = true) {
23
+ const slug = slugify(input, allowSlash);
24
+ if (!slug) {
25
+ throw new Error('Slug is empty after slugification.');
26
+ }
27
+ if (slug === input) {
28
+ return slug;
29
+ }
30
+ const confirmed = await promptConfirm(`Use slugified input "${slug}" instead of "${input}"?`);
31
+ if (!confirmed) {
32
+ throw new Error('Slug confirmation declined.');
33
+ }
34
+ return slug;
35
+ }
@@ -0,0 +1,115 @@
1
+ import { readFile } from 'fs/promises';
2
+ import { fileURLToPath } from 'node:url';
3
+ import os from 'os';
4
+ import path from 'path';
5
+ import { FEAT_FORGE_CONFIG_FOLDER } from './constants.js';
6
+ import { pathExists, readTextFile } from './fs.js';
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ export const SOURCE_TEMPLATE_PATH = path.join(__dirname, '..', 'templates');
10
+ export const SOURCE_TEMPLATE_AGENT_PATH = path.join(SOURCE_TEMPLATE_PATH, 'agent');
11
+ /**
12
+ * Return the built-in fallback template for a given spec file.
13
+ */
14
+ export async function defaultFeatForgeTemplateForSpecFile(templateFileName) {
15
+ const templateFilePath = path.join(SOURCE_TEMPLATE_PATH, templateFileName);
16
+ // console.log('Looking for built-in template at', templateFilePath);
17
+ if (await pathExists(templateFilePath)) {
18
+ try {
19
+ return readTextFile(templateFilePath);
20
+ }
21
+ catch (error) {
22
+ console.error('Could not read template file, using fallback template. Error:', error);
23
+ }
24
+ }
25
+ // Fallback templates if built-in files are not found for some reason... (shouldn't happen in normal usage since templates are bundled, but just in case)
26
+ switch (templateFileName) {
27
+ case 'SPEC.md':
28
+ return `# Specifications for [Feature Title]`;
29
+ case 'TODO.md':
30
+ return '# TODO\n\n* [ ] \n';
31
+ default:
32
+ return `# ${templateFileName}`;
33
+ }
34
+ }
35
+ export async function defaultFeatForgeTemplateForAgentFile(agentFileName) {
36
+ const templateFilePath = path.join(SOURCE_TEMPLATE_AGENT_PATH, agentFileName);
37
+ // console.log('Looking for built-in template at', templateFilePath);
38
+ if (await pathExists(templateFilePath)) {
39
+ try {
40
+ return readTextFile(templateFilePath);
41
+ }
42
+ catch (error) {
43
+ console.error('Could not read agent template file, using fallback template. Error:', error);
44
+ }
45
+ }
46
+ return `# ${agentFileName}`;
47
+ }
48
+ /**
49
+ * Resolve a template file from repo or user overrides.
50
+ * Order: repo .features/.template -> ~/.feat-forge/template -> built-in.
51
+ */
52
+ export async function resolveSpecFileCustomTemplate(forgeContext, repository, fileName, subdir = '') {
53
+ // User override in the project .feat-forge/.template/
54
+ const projectTemplate = path.join(forgeContext.rootDir, FEAT_FORGE_CONFIG_FOLDER, '.template', subdir, fileName);
55
+ if (await pathExists(projectTemplate)) {
56
+ return readFile(projectTemplate, 'utf8');
57
+ }
58
+ // Repo override in .specs/.template/ for all developers working on the repo (should be committed to git)
59
+ const repoTemplate = repository.getTemplatePath(subdir, fileName);
60
+ if (await pathExists(repoTemplate)) {
61
+ return readFile(repoTemplate, 'utf8');
62
+ }
63
+ // User override in the home directory ~/.feat-forge/.template/ (not committed, only for the current user)
64
+ // Basically if the user want's to override all FeatForge templates on their machine, they can put the templates in ~/.feat-forge/.template/
65
+ const userTemplate = path.join(os.homedir(), FEAT_FORGE_CONFIG_FOLDER, '.template', subdir, fileName);
66
+ if (await pathExists(userTemplate)) {
67
+ return readFile(userTemplate, 'utf8');
68
+ }
69
+ // Fallback to built-in templates
70
+ return await defaultFeatForgeTemplateForSpecFile(fileName);
71
+ }
72
+ export async function resolveAgentFileCustomTemplate(forgeContext, repository, agentFile, subdir = '') {
73
+ // User override in the project .feat-forge/.template/
74
+ const projectTemplate = path.join(forgeContext.rootDir, FEAT_FORGE_CONFIG_FOLDER, '.template', '.forge-agents', subdir, agentFile);
75
+ // console.log('Looking for agent template at', projectTemplate);
76
+ if (await pathExists(projectTemplate)) {
77
+ return readFile(projectTemplate, 'utf8');
78
+ }
79
+ // Repo override in .specs/.template/.forge-agents/ for all developers working on the repo (should be committed to git)
80
+ const repoTemplate = repository.getAgentTemplatePath(subdir, agentFile);
81
+ // console.log('Looking for agent template at', repoTemplate);
82
+ if (await pathExists(repoTemplate)) {
83
+ return readFile(repoTemplate, 'utf8');
84
+ }
85
+ // User override in the home directory ~/.feat-forge/.template/.forge-agents/
86
+ // Basically if the user want's to override all FeatForge templates on their machine, they can put the templates in ~/.feat-forge/.template/
87
+ const userTemplate = path.join(os.homedir(), FEAT_FORGE_CONFIG_FOLDER, '.template', '.forge-agents', subdir, agentFile);
88
+ // console.log('Looking for agent template at', userTemplate);
89
+ if (await pathExists(userTemplate)) {
90
+ return readFile(userTemplate, 'utf8');
91
+ }
92
+ // Fallback to built-in templates
93
+ return await defaultFeatForgeTemplateForAgentFile(agentFile);
94
+ }
95
+ export function replaceTemplateMarkers(templateContent, forgeContext, branchContext, repoContext) {
96
+ // replace %%--XXXXXXXXXXX--%% markers in the template with the corresponding content from infos
97
+ const regex = /%%--([A-Z_]+)--%%/g;
98
+ let match;
99
+ let result = templateContent;
100
+ const activeSpecFolder = forgeContext.options.folders.activeSpec;
101
+ while ((match = regex.exec(result)) !== null) {
102
+ const marker = match[1];
103
+ switch (marker) {
104
+ case 'COPILOT_SPEC_FILES':
105
+ // FIXME: path resolution here is hacky
106
+ const relativePath = repoContext ? '../..' : '.';
107
+ const replacement = forgeContext.config.specFiles
108
+ .map((file) => `#file:${relativePath}/${activeSpecFolder}/${file}`)
109
+ .join('\n');
110
+ result = result.replace(match[0], replacement);
111
+ break;
112
+ }
113
+ }
114
+ return result;
115
+ }
@@ -0,0 +1,15 @@
1
+ import { plainToInstance } from 'class-transformer';
2
+ import { validate } from 'class-validator';
3
+ export async function validateInput(classTransformerClass, data, options) {
4
+ // Validate and transform using class-validator and class-transformer
5
+ const extractedInstance = plainToInstance(classTransformerClass, data);
6
+ const errors = await validate(extractedInstance, {
7
+ whitelist: true,
8
+ validationError: { target: true, value: true },
9
+ ...options,
10
+ });
11
+ if (errors.length > 0) {
12
+ throw new Error(`Invalid ${classTransformerClass.name} file :\n${JSON.stringify(errors, null, 2)}`);
13
+ }
14
+ return extractedInstance;
15
+ }
@@ -0,0 +1,21 @@
1
+ # Goal
2
+
3
+ ---
4
+
5
+ # Feature details
6
+
7
+ -
8
+ -
9
+ -
10
+
11
+ # Acceptance criteria
12
+
13
+ -
14
+ -
15
+ -
16
+
17
+ # Not in the perimeter
18
+
19
+ -
20
+ -
21
+ -
@@ -0,0 +1,5 @@
1
+ # TODO
2
+
3
+ - [ ]
4
+ - [ ]
5
+ - [ ]
@@ -0,0 +1,4 @@
1
+ # Agent **Omnibus**
2
+
3
+ You are a generalist agent that can do a bit of everything.
4
+ You can execute code, read and edit files, search the web, use other tools and agents, and manage your own todo list.
@@ -0,0 +1,4 @@
1
+ # Agent **Inventorius**
2
+
3
+ Help to define a functional perimeter for a feature by asking questions to the user
4
+ and doing research online to define what are the good ideas and good practices around the subject
@@ -0,0 +1,8 @@
1
+ # Agent **Architectus**
2
+
3
+ Help to define a clean, maintainable code structure for the feature:
4
+
5
+ - propose an architecture and module boundaries
6
+ - suggest patterns that match the project stack
7
+ - identify integration points and risks (DX, performance, security)
8
+ - keep the design as simple as possible while future-proofing key decisions
@@ -0,0 +1,8 @@
1
+ # Agent **Strategos**
2
+
3
+ Generate a complete, actionable implementation plan:
4
+
5
+ - break the feature into clear steps and milestones
6
+ - list tasks with acceptance criteria and edge cases
7
+ - propose an order of execution that reduces risk
8
+ - highlight dependencies, migrations, and rollback strategies
@@ -0,0 +1,8 @@
1
+ # Agent **TestDrivenCodificius**
2
+
3
+ Implement using a Test-Driven Development approach by orchestrating sub-agents:
4
+
5
+ - write tests first (via **TestScriptor**)
6
+ - implement the minimal code to pass tests (via **Codificius**)
7
+ - refactor safely (via **Consolidarius**) while keeping tests green
8
+ - rely on **TestExecutor** to run the suite frequently and confirm behavior
@@ -0,0 +1,8 @@
1
+ # Agent **Codificius**
2
+
3
+ Implement the feature in a straightforward coding approach:
4
+
5
+ - translate the plan into working code with minimal complexity
6
+ - prefer readability and consistency with the existing codebase
7
+ - keep changes scoped to the feature and avoid over-engineering
8
+ - write small, reviewable commits and leave the code in a shippable state
@@ -0,0 +1,8 @@
1
+ # Agent **Consolidarius**
2
+
3
+ Simplify and consolidate the produced code without changing behavior:
4
+
5
+ - refactor duplicated code into reusable helpers/modules
6
+ - reduce complexity and improve readability
7
+ - introduce generic error handlers where appropriate
8
+ - align patterns across the codebase and remove unnecessary boilerplate
@@ -0,0 +1,8 @@
1
+ # Agent **Auditorix**
2
+
3
+ Review the code to ensure it meets project standards:
4
+
5
+ - verify style, conventions, and architecture alignment
6
+ - detect code smells, duplicated logic, and missing edge cases
7
+ - check security and reliability basics (inputs, errors, timeouts, logging)
8
+ - ensure the implementation matches the plan/spec and is easy to maintain
@@ -0,0 +1,8 @@
1
+ # Agent **TestScriptor**
2
+
3
+ Write unit tests (and only tests) for the feature:
4
+
5
+ - cover expected behavior, edge cases, and regressions
6
+ - use the project’s testing conventions and utilities
7
+ - keep tests readable and focused
8
+ - avoid changing production code unless explicitly requested
@@ -0,0 +1,8 @@
1
+ # Agent **TestExecutor**
2
+
3
+ Execute the test suite and validate runtime behavior:
4
+
5
+ - run unit/integration tests as relevant
6
+ - report failures with actionable diagnosis (what failed, why likely, where)
7
+ - suggest the minimal fix and verify it by re-running tests
8
+ - confirm the feature works end-to-end when applicable
@@ -0,0 +1,10 @@
1
+ # Agent **Scribus**
2
+
3
+ Propose high-quality commit messages and prepare commits for validation:
4
+
5
+ - summarize changes clearly (scope + intent)
6
+ - follow the project’s commit convention (e.g., Conventional Commits) if used
7
+ - group commits logically (tests, refactor, feature, chore)
8
+ - ensure the diff is clean before committing (no stray formatting/debug code)
9
+
10
+ Do not commit until user validation is received.