feat-forge 1.1.0 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -5
- package/dist/commands/CompletionCommands.js +20 -41
- package/dist/foundation/PortAllocator.js +2 -0
- package/dist/foundation/Proxy.js +5 -2
- package/dist/foundation/types/Services.js +6 -0
- package/dist/lib/proxy-dashboard.js +51 -27
- package/dist/lib/services.js +2 -0
- package/dist/templates/agent/CONTEXT.code.md +7 -7
- package/dist/templates/agent/CONTEXT.spec.md +9 -9
- package/dist/templates/agent/Copilot/Specs.agent.md +3 -3
- package/dist/templates/agent/Copilot/SpecsCommit.agent.md +1 -1
- package/dist/templates/agent/Copilot/TODO-Reader.agent.md +2 -2
- package/package.json +18 -15
package/README.md
CHANGED
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
|
|
11
11
|
Its goal is to make the specification of features explicit and separate the thinking/specifying phase from the coding/implementation phase, across multiple agents and repositories, while keeping everything organized and traceable.
|
|
12
12
|
|
|
13
|
-
With **FeatForge** you will be able to
|
|
13
|
+
With **FeatForge** you will be able to:
|
|
14
14
|
|
|
15
15
|
- Parallelize work on multiple features:
|
|
16
16
|
- across multiple repositories.
|
|
17
|
-
-
|
|
17
|
+
- across multiple agents.
|
|
18
18
|
- while keeping track of everything.
|
|
19
19
|
- Come back to any feature after days/weeks and immediately understand its initial specifications.
|
|
20
20
|
- Change agent configurations and prompts on a per-feature basis.
|
|
@@ -26,7 +26,7 @@ With **FeatForge** you will be able to :
|
|
|
26
26
|
- Not opinionated about how you specify features, how you implement them, or how you use agents. It just provides a structure (yet customizable) and a workflow to keep everything organized and traceable.
|
|
27
27
|
- Partially tested with : Copilot, Codex, Claude code (+Ollama, LM-Studio). But it should work with any agent that can be configured to read/write files in the `.active-spec` folders.
|
|
28
28
|
|
|
29
|
-
**_This is an
|
|
29
|
+
**_This is an experiment, trying to mix between classic development and vibe-coding in large project with quality and sustainability in mind by making specification explicit and separating thinking from coding across multiple agents and repositories._**
|
|
30
30
|
|
|
31
31
|
## Key Concepts
|
|
32
32
|
|
|
@@ -43,7 +43,7 @@ With **FeatForge** you will be able to :
|
|
|
43
43
|
## Installation
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
-
npm install -g feat-forge
|
|
46
|
+
npm install -g feat-forge
|
|
47
47
|
forge --version
|
|
48
48
|
```
|
|
49
49
|
|
|
@@ -116,7 +116,7 @@ forge-project-root/
|
|
|
116
116
|
## Modes
|
|
117
117
|
|
|
118
118
|
Each branch has a mode stored in `.specs/<slug>/.forge-mode`. Switching modes updates agent adapter files with the corresponding templates.
|
|
119
|
-
Agent
|
|
119
|
+
Agent names are useful when calling subagents.
|
|
120
120
|
|
|
121
121
|
Built-in modes:
|
|
122
122
|
|
|
@@ -302,6 +302,23 @@ Refresh agent adapter files for the nearest branch.
|
|
|
302
302
|
|
|
303
303
|
Scan repositories for service declarations and generate configuration with allocated ports.
|
|
304
304
|
|
|
305
|
+
Service declarations are read from `.forge/services.json` in each repository. You can define an optional dedicated health endpoint per service:
|
|
306
|
+
|
|
307
|
+
```json
|
|
308
|
+
{
|
|
309
|
+
"services": [
|
|
310
|
+
{
|
|
311
|
+
"name": "api",
|
|
312
|
+
"type": "http",
|
|
313
|
+
"path": "/api",
|
|
314
|
+
"healthCheckPath": "/health"
|
|
315
|
+
}
|
|
316
|
+
]
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
If `healthCheckPath` is omitted, proxy health checks fallback to `path`, then `/`.
|
|
321
|
+
|
|
305
322
|
---
|
|
306
323
|
|
|
307
324
|
### `forge services list [branch]`
|
|
@@ -355,6 +372,10 @@ For all commands and options:
|
|
|
355
372
|
forge --help
|
|
356
373
|
```
|
|
357
374
|
|
|
375
|
+
## Contributing
|
|
376
|
+
|
|
377
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup and contribution workflow.
|
|
378
|
+
|
|
358
379
|
# License
|
|
359
380
|
|
|
360
381
|
[](./LICENSE)
|
|
@@ -176,18 +176,10 @@ try{
|
|
|
176
176
|
const envCommands = data.envCmd?.subcommands.map((cmd) => cmd.name).join(' ') || '';
|
|
177
177
|
const maintenanceCommands = data.maintenanceCmd?.subcommands.map((cmd) => cmd.name).join(' ') || '';
|
|
178
178
|
const agentCommands = data.agentCmd?.subcommands.map((cmd) => cmd.name).join(' ') || '';
|
|
179
|
-
const featureSlugCmds = data.featureCmd?.subcommands
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
.filter((cmd) => data.slugSubcommands.includes(cmd.name))
|
|
184
|
-
.map((cmd) => cmd.name) || [];
|
|
185
|
-
const releaseSlugCmds = data.releaseCmd?.subcommands
|
|
186
|
-
.filter((cmd) => data.slugSubcommands.includes(cmd.name))
|
|
187
|
-
.map((cmd) => cmd.name) || [];
|
|
188
|
-
const mainSlugCmds = data.mainCommands
|
|
189
|
-
.filter((cmd) => data.rootSlugCommands.includes(cmd.name))
|
|
190
|
-
.map((cmd) => cmd.name);
|
|
179
|
+
const featureSlugCmds = data.featureCmd?.subcommands.filter((cmd) => data.slugSubcommands.includes(cmd.name)).map((cmd) => cmd.name) || [];
|
|
180
|
+
const fixSlugCmds = data.fixCmd?.subcommands.filter((cmd) => data.slugSubcommands.includes(cmd.name)).map((cmd) => cmd.name) || [];
|
|
181
|
+
const releaseSlugCmds = data.releaseCmd?.subcommands.filter((cmd) => data.slugSubcommands.includes(cmd.name)).map((cmd) => cmd.name) || [];
|
|
182
|
+
const mainSlugCmds = data.mainCommands.filter((cmd) => data.rootSlugCommands.includes(cmd.name)).map((cmd) => cmd.name);
|
|
191
183
|
const featureSlugCase = featureSlugCmds.length > 0
|
|
192
184
|
? ` ${featureSlugCmds.join('|')})
|
|
193
185
|
if [[ \${cword} -eq 3 ]]; then
|
|
@@ -464,26 +456,22 @@ complete -F _forge_completion forge
|
|
|
464
456
|
// ============================================================================
|
|
465
457
|
generateZshCompletion() {
|
|
466
458
|
const data = this.getCompletionData();
|
|
467
|
-
const commandsArray = data.mainCommands
|
|
459
|
+
const commandsArray = data.mainCommands
|
|
460
|
+
.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`)
|
|
461
|
+
.join('\n');
|
|
468
462
|
const featureArray = data.featureCmd?.subcommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n') || '';
|
|
469
463
|
const fixArray = data.fixCmd?.subcommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n') || '';
|
|
470
464
|
const releaseArray = data.releaseCmd?.subcommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n') || '';
|
|
471
|
-
const servicesArray = data.servicesCmd?.subcommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n') ||
|
|
465
|
+
const servicesArray = data.servicesCmd?.subcommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n') ||
|
|
466
|
+
'';
|
|
472
467
|
const envArray = data.envCmd?.subcommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n') || '';
|
|
473
|
-
const maintenanceArray = data.maintenanceCmd?.subcommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n') ||
|
|
468
|
+
const maintenanceArray = data.maintenanceCmd?.subcommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n') ||
|
|
469
|
+
'';
|
|
474
470
|
const agentArray = data.agentCmd?.subcommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n') || '';
|
|
475
|
-
const featureSlugCmds = data.featureCmd?.subcommands
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const
|
|
479
|
-
.filter((cmd) => data.slugSubcommands.includes(cmd.name))
|
|
480
|
-
.map((cmd) => cmd.name) || [];
|
|
481
|
-
const releaseSlugCmds = data.releaseCmd?.subcommands
|
|
482
|
-
.filter((cmd) => data.slugSubcommands.includes(cmd.name))
|
|
483
|
-
.map((cmd) => cmd.name) || [];
|
|
484
|
-
const mainSlugCmds = data.mainCommands
|
|
485
|
-
.filter((cmd) => data.rootSlugCommands.includes(cmd.name))
|
|
486
|
-
.map((cmd) => cmd.name);
|
|
471
|
+
const featureSlugCmds = data.featureCmd?.subcommands.filter((cmd) => data.slugSubcommands.includes(cmd.name)).map((cmd) => cmd.name) || [];
|
|
472
|
+
const fixSlugCmds = data.fixCmd?.subcommands.filter((cmd) => data.slugSubcommands.includes(cmd.name)).map((cmd) => cmd.name) || [];
|
|
473
|
+
const releaseSlugCmds = data.releaseCmd?.subcommands.filter((cmd) => data.slugSubcommands.includes(cmd.name)).map((cmd) => cmd.name) || [];
|
|
474
|
+
const mainSlugCmds = data.mainCommands.filter((cmd) => data.rootSlugCommands.includes(cmd.name)).map((cmd) => cmd.name);
|
|
487
475
|
const featureSlugCase = featureSlugCmds.length > 0
|
|
488
476
|
? ` ${featureSlugCmds.join('|')})
|
|
489
477
|
local -a slugs
|
|
@@ -793,8 +781,7 @@ compdef _forge forge
|
|
|
793
781
|
const fixSlugCompletions = genSlugCompletions('fix', data.fixCmd, '__forge_fix_slugs');
|
|
794
782
|
const releaseSlugCompletions = genSlugCompletions('release', data.releaseCmd, '__forge_release_slugs');
|
|
795
783
|
// Root-level slug commands
|
|
796
|
-
const mainSlugCmds = data.mainCommands
|
|
797
|
-
.filter((cmd) => data.rootSlugCommands.includes(cmd.name));
|
|
784
|
+
const mainSlugCmds = data.mainCommands.filter((cmd) => data.rootSlugCommands.includes(cmd.name));
|
|
798
785
|
const mainSlugCompletions = mainSlugCmds
|
|
799
786
|
.map((cmd) => `complete -c forge -n "__fish_seen_subcommand_from ${cmd.name}" -a "(__forge_all_branches)"`)
|
|
800
787
|
.join('\n');
|
|
@@ -1001,18 +988,10 @@ complete -c forge -n "__fish_seen_subcommand_from alias" -a pwsh -d "Generate Po
|
|
|
1001
988
|
const envCommandsList = toPsArray(data.envCmd?.subcommands.map((cmd) => cmd.name) || []);
|
|
1002
989
|
const maintenanceCommandsList = toPsArray(data.maintenanceCmd?.subcommands.map((cmd) => cmd.name) || []);
|
|
1003
990
|
const agentCommandsList = toPsArray(data.agentCmd?.subcommands.map((cmd) => cmd.name) || []);
|
|
1004
|
-
const featureSlugCommandsList = toPsArray(data.featureCmd?.subcommands
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
const
|
|
1008
|
-
.filter((cmd) => data.slugSubcommands.includes(cmd.name))
|
|
1009
|
-
.map((cmd) => cmd.name) || []);
|
|
1010
|
-
const releaseSlugCommandsList = toPsArray(data.releaseCmd?.subcommands
|
|
1011
|
-
.filter((cmd) => data.slugSubcommands.includes(cmd.name))
|
|
1012
|
-
.map((cmd) => cmd.name) || []);
|
|
1013
|
-
const mainSlugCommandsList = toPsArray(data.mainCommands
|
|
1014
|
-
.filter((cmd) => data.rootSlugCommands.includes(cmd.name))
|
|
1015
|
-
.map((cmd) => cmd.name));
|
|
991
|
+
const featureSlugCommandsList = toPsArray(data.featureCmd?.subcommands.filter((cmd) => data.slugSubcommands.includes(cmd.name)).map((cmd) => cmd.name) || []);
|
|
992
|
+
const fixSlugCommandsList = toPsArray(data.fixCmd?.subcommands.filter((cmd) => data.slugSubcommands.includes(cmd.name)).map((cmd) => cmd.name) || []);
|
|
993
|
+
const releaseSlugCommandsList = toPsArray(data.releaseCmd?.subcommands.filter((cmd) => data.slugSubcommands.includes(cmd.name)).map((cmd) => cmd.name) || []);
|
|
994
|
+
const mainSlugCommandsList = toPsArray(data.mainCommands.filter((cmd) => data.rootSlugCommands.includes(cmd.name)).map((cmd) => cmd.name));
|
|
1016
995
|
const completionShellsList = toPsArray(['bash', 'zsh', 'fish', 'powershell', 'pwsh']);
|
|
1017
996
|
return `# forge PowerShell completion script
|
|
1018
997
|
|
|
@@ -75,6 +75,8 @@ export class BranchPortAllocation {
|
|
|
75
75
|
const serviceName = service.name;
|
|
76
76
|
const existingService = this.getService(serviceName);
|
|
77
77
|
if (existingService) {
|
|
78
|
+
// Keep the allocated port stable, but refresh service metadata from latest scan
|
|
79
|
+
Object.assign(existingService, service);
|
|
78
80
|
return existingService.port;
|
|
79
81
|
}
|
|
80
82
|
const nextPort = this.nextAvailablePort;
|
package/dist/foundation/Proxy.js
CHANGED
|
@@ -28,10 +28,12 @@ export class Proxy {
|
|
|
28
28
|
this.routingTable = await this.buildRoutingTable(branchContexts);
|
|
29
29
|
this.showSummary(port);
|
|
30
30
|
this.startServer(port);
|
|
31
|
-
this.startWatching();
|
|
32
31
|
console.log(`\n🚀 Proxy server running on http://localhost:${port}`);
|
|
33
32
|
console.log(`📊 Dashboard: http://localhost:${port}`);
|
|
34
33
|
console.log('\nPress Ctrl+C to stop.\n');
|
|
34
|
+
// Watchers can take noticeable time to initialize on large trees.
|
|
35
|
+
// Start them after startup logs so the proxy appears ready immediately.
|
|
36
|
+
setImmediate(() => this.startWatching());
|
|
35
37
|
}
|
|
36
38
|
stop() {
|
|
37
39
|
this.stopWatching?.();
|
|
@@ -47,12 +49,13 @@ export class Proxy {
|
|
|
47
49
|
try {
|
|
48
50
|
const generated = await loadGeneratedServicesFile(branchContext);
|
|
49
51
|
for (const service of generated.services) {
|
|
50
|
-
const { proxyUrl, url, key, name } = getServiceOutputs(this.context, branchContext, service);
|
|
52
|
+
const { proxyUrl, url, key, name, healthCheckUrl } = getServiceOutputs(this.context, branchContext, service);
|
|
51
53
|
table.set(key, {
|
|
52
54
|
branchName: branchContext.branchName,
|
|
53
55
|
serviceName: name,
|
|
54
56
|
url,
|
|
55
57
|
proxyUrl,
|
|
58
|
+
healthCheckUrl,
|
|
56
59
|
});
|
|
57
60
|
}
|
|
58
61
|
}
|
|
@@ -16,6 +16,7 @@ export class ServiceDefinition {
|
|
|
16
16
|
name;
|
|
17
17
|
type = 'http'; // Phase 1: HTTP only, but extensible
|
|
18
18
|
path; // HTTP only, e.g., "/api"
|
|
19
|
+
healthCheckPath; // HTTP only, e.g., "/health"
|
|
19
20
|
}
|
|
20
21
|
__decorate([
|
|
21
22
|
IsString(),
|
|
@@ -31,6 +32,11 @@ __decorate([
|
|
|
31
32
|
IsString(),
|
|
32
33
|
__metadata("design:type", String)
|
|
33
34
|
], ServiceDefinition.prototype, "path", void 0);
|
|
35
|
+
__decorate([
|
|
36
|
+
IsOptional(),
|
|
37
|
+
IsString(),
|
|
38
|
+
__metadata("design:type", String)
|
|
39
|
+
], ServiceDefinition.prototype, "healthCheckPath", void 0);
|
|
34
40
|
/**
|
|
35
41
|
* Services declaration from .forge/services.json
|
|
36
42
|
*/
|
|
@@ -1,24 +1,9 @@
|
|
|
1
|
-
import http from 'http';
|
|
2
|
-
async function checkHealth(target) {
|
|
3
|
-
return new Promise((resolve) => {
|
|
4
|
-
const req = http.get(target, { timeout: 2000 }, (res) => {
|
|
5
|
-
res.resume();
|
|
6
|
-
resolve(true);
|
|
7
|
-
});
|
|
8
|
-
req.on('error', () => resolve(false));
|
|
9
|
-
req.on('timeout', () => {
|
|
10
|
-
req.destroy();
|
|
11
|
-
resolve(false);
|
|
12
|
-
});
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
1
|
export async function handleDashboardRequest(req, res, routingTable, proxyPort) {
|
|
16
2
|
const statuses = [];
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
statuses.push({
|
|
3
|
+
Array.from(routingTable.entries()).forEach(([, route], index) => {
|
|
4
|
+
const rowId = `${route.branchName}::${route.serviceName}::${index}`;
|
|
5
|
+
statuses.push({ id: encodeURIComponent(rowId), route, status: 'PENDING' });
|
|
20
6
|
});
|
|
21
|
-
await Promise.all(checks);
|
|
22
7
|
statuses.sort((a, b) => a.route.branchName.localeCompare(b.route.branchName) || a.route.serviceName.localeCompare(b.route.serviceName));
|
|
23
8
|
const grouped = new Map();
|
|
24
9
|
for (const s of statuses) {
|
|
@@ -28,27 +13,28 @@ export async function handleDashboardRequest(req, res, routingTable, proxyPort)
|
|
|
28
13
|
}
|
|
29
14
|
const rows = Array.from(grouped.entries())
|
|
30
15
|
.map(([branchName, services]) => {
|
|
31
|
-
const header = `<tr class="branch-header"><td colspan="
|
|
16
|
+
const header = `<tr class="branch-header"><td colspan="5"><strong>${branchName}</strong></td></tr>`;
|
|
32
17
|
const serviceRows = services
|
|
33
18
|
.map((s) => {
|
|
34
|
-
|
|
35
|
-
? '<span style="color:#22c55e;font-weight:bold">● UP</span>'
|
|
36
|
-
: '<span style="color:#ef4444;font-weight:bold">● DOWN</span>';
|
|
37
|
-
return `<tr>
|
|
19
|
+
return `<tr data-health-row="${s.id}">
|
|
38
20
|
<td>${s.route.serviceName}</td>
|
|
39
21
|
<td><a href="${s.route.proxyUrl}" target="_blank">${s.route.proxyUrl}</a></td>
|
|
40
22
|
<td><a href="${s.route.url}" target="_blank">${s.route.url}</a></td>
|
|
41
|
-
<td>${
|
|
23
|
+
<td><a href="${s.route.healthCheckUrl}" target="_blank">${s.route.healthCheckUrl}</a></td>
|
|
24
|
+
<td data-health-status="${s.id}"><span style="color:#f59e0b;font-weight:bold">● ${s.status}</span></td>
|
|
42
25
|
</tr>`;
|
|
43
26
|
})
|
|
44
27
|
.join('\n');
|
|
45
28
|
return header + '\n' + serviceRows;
|
|
46
29
|
})
|
|
47
30
|
.join('\n');
|
|
31
|
+
const healthTargets = statuses.map((s) => ({
|
|
32
|
+
id: s.id,
|
|
33
|
+
url: s.route.healthCheckUrl,
|
|
34
|
+
}));
|
|
48
35
|
const html = `<!DOCTYPE html>
|
|
49
36
|
<html><head>
|
|
50
37
|
<meta charset="utf-8">
|
|
51
|
-
<meta http-equiv="refresh" content="30">
|
|
52
38
|
<title>Feat-Forge Proxy Dashboard</title>
|
|
53
39
|
<style>
|
|
54
40
|
body { font-family: system-ui, sans-serif; margin: 2rem; background: #0f172a; color: #e2e8f0; }
|
|
@@ -66,9 +52,47 @@ export async function handleDashboardRequest(req, res, routingTable, proxyPort)
|
|
|
66
52
|
<h1>Feat-Forge Proxy Dashboard</h1>
|
|
67
53
|
<p class="meta">${statuses.length} routes · auto-refresh 30s · proxy port ${proxyPort}</p>
|
|
68
54
|
<table>
|
|
69
|
-
<thead><tr><th>Service</th><th>Proxy URL</th><th>Target</th><th>Status</th></tr></thead>
|
|
70
|
-
<tbody>${rows || '<tr><td colspan="
|
|
55
|
+
<thead><tr><th>Service</th><th>Proxy URL</th><th>Target</th><th>Health Check</th><th>Status</th></tr></thead>
|
|
56
|
+
<tbody>${rows || '<tr><td colspan="5" style="text-align:center;color:#64748b">No routes configured. Run <code>forge services scan</code> on active branches.</td></tr>'}</tbody>
|
|
71
57
|
</table>
|
|
58
|
+
<script>
|
|
59
|
+
const healthTargets = ${JSON.stringify(healthTargets)};
|
|
60
|
+
const HEALTH_TIMEOUT_MS = 1200;
|
|
61
|
+
const REFRESH_EVERY_MS = 30000;
|
|
62
|
+
|
|
63
|
+
function setStatus(id, label, color) {
|
|
64
|
+
const el = document.querySelector('[data-health-status="' + id + '"]');
|
|
65
|
+
if (!el) return;
|
|
66
|
+
el.innerHTML = '<span style="color:' + color + ';font-weight:bold">● ' + label + '</span>';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function checkHealth(url) {
|
|
70
|
+
const controller = new AbortController();
|
|
71
|
+
const timer = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
|
|
72
|
+
try {
|
|
73
|
+
// no-cors allows probing reachability from the browser without requiring CORS headers.
|
|
74
|
+
await fetch(url, { mode: 'no-cors', cache: 'no-store', signal: controller.signal });
|
|
75
|
+
return true;
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
} finally {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function refreshStatuses() {
|
|
84
|
+
await Promise.all(healthTargets.map(async ({ id, url }) => {
|
|
85
|
+
setStatus(id, 'PENDING', '#f59e0b');
|
|
86
|
+
const healthy = await checkHealth(url);
|
|
87
|
+
setStatus(id, healthy ? 'UP' : 'DOWN', healthy ? '#22c55e' : '#ef4444');
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (healthTargets.length > 0) {
|
|
92
|
+
refreshStatuses();
|
|
93
|
+
setInterval(refreshStatuses, REFRESH_EVERY_MS);
|
|
94
|
+
}
|
|
95
|
+
</script>
|
|
72
96
|
</body></html>`;
|
|
73
97
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
74
98
|
res.end(html);
|
package/dist/lib/services.js
CHANGED
|
@@ -119,6 +119,7 @@ export function getServiceOutputs(forgeContext, branchContext, service) {
|
|
|
119
119
|
const urlSlug = slugify(service.name, false);
|
|
120
120
|
const branchSlug = branchContext.branchNameSlug;
|
|
121
121
|
const proxyPort = forgeContext.options.proxy.port;
|
|
122
|
+
const healthCheckPath = service.healthCheckPath || service.path || '/';
|
|
122
123
|
return {
|
|
123
124
|
...service,
|
|
124
125
|
branchSlug,
|
|
@@ -128,5 +129,6 @@ export function getServiceOutputs(forgeContext, branchContext, service) {
|
|
|
128
129
|
urlSlug: urlSlug,
|
|
129
130
|
url: `${service.type}://localhost:${service.port}${service.path || ''}`,
|
|
130
131
|
proxyUrl: `${service.type}://${branchSlug}.${urlSlug}.localhost:${proxyPort}${service.path || ''}`,
|
|
132
|
+
healthCheckUrl: `${service.type}://localhost:${service.port}${healthCheckPath}`,
|
|
131
133
|
};
|
|
132
134
|
}
|
|
@@ -4,8 +4,8 @@ This project is **process-first**: changes must be deliberate, reviewable, and v
|
|
|
4
4
|
|
|
5
5
|
## Golden Rules (must follow)
|
|
6
6
|
|
|
7
|
-
1. **Read the feature spec files first (in this order), they are in the .active-
|
|
8
|
-
- `../
|
|
7
|
+
1. **Read the feature spec files first (in this order), they are in the .active-spec folder:**
|
|
8
|
+
- `../SPEC.md`
|
|
9
9
|
- `../TODO.md`
|
|
10
10
|
|
|
11
11
|
2. **Do not implement code blindly.**
|
|
@@ -26,7 +26,7 @@ This project is **process-first**: changes must be deliberate, reviewable, and v
|
|
|
26
26
|
|
|
27
27
|
5. **Always keep the spec in sync.**
|
|
28
28
|
- If you implement something, update `TODO.md` (checklist / status).
|
|
29
|
-
- If you make a design choice, record it in a sub section in `
|
|
29
|
+
- If you make a design choice, record it in a sub section in `SPEC.md`.
|
|
30
30
|
|
|
31
31
|
6. **No secrets / no destructive actions.**
|
|
32
32
|
- Don’t touch credentials, `.env`, keys, or user-specific configs.
|
|
@@ -39,7 +39,7 @@ Protect your context window, use subagent to protecte it
|
|
|
39
39
|
1. **Read specifications**
|
|
40
40
|
|
|
41
41
|
- Use a subagent **TODO Reader** to read the specifications files and extract actionable tasks, clarifying and prioritizing them as needed :
|
|
42
|
-
- `../
|
|
42
|
+
- `../SPEC.md`
|
|
43
43
|
- `../TODO.md`
|
|
44
44
|
- Use it to define clear independant code tasks
|
|
45
45
|
|
|
@@ -62,11 +62,11 @@ Protect your context window, use subagent to protecte it
|
|
|
62
62
|
|
|
63
63
|
Its a TODO Reader agent for the current feature.
|
|
64
64
|
|
|
65
|
-
His job is to extract, clarify, and prioritize actionable tasks from `TODO.md`, based on concepts in `
|
|
65
|
+
His job is to extract, clarify, and prioritize actionable tasks from `TODO.md`, based on concepts in `SPEC.md`. Specs files are here : - `../SPEC.md` - `../TODO.md`
|
|
66
66
|
|
|
67
67
|
#### Responsibilities
|
|
68
68
|
|
|
69
|
-
- Read and understand the feature specification in `
|
|
69
|
+
- Read and understand the feature specification in `SPEC.md`.
|
|
70
70
|
- Read and understand every item in `TODO.md`.
|
|
71
71
|
- Clarify ambiguities by asking questions if needed.
|
|
72
72
|
- Output a clear, actionable list of tasks for implementation by other agents, ensuring each task is specific, measurable, and feasible.
|
|
@@ -138,7 +138,7 @@ A step is “done” only if:
|
|
|
138
138
|
- The changes are implemented
|
|
139
139
|
- `TODO.md` is updated accordingly
|
|
140
140
|
- Verification steps are provided (and ideally runnable)
|
|
141
|
-
- Any new major design choice is recorded in `
|
|
141
|
+
- Any new major design choice is recorded in `SPEC.md`
|
|
142
142
|
|
|
143
143
|
## If something is missing
|
|
144
144
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This project is **process-first**: clarity before code, all changes must go through the feature specification files:
|
|
4
4
|
|
|
5
|
-
- `../
|
|
5
|
+
- `../SPEC.md`
|
|
6
6
|
- `../TODO.md`
|
|
7
7
|
|
|
8
8
|
You are currently in **SPEC MODE** / **PLAN MODE**.
|
|
@@ -14,13 +14,13 @@ You must only modify the specification files and not touch application code.
|
|
|
14
14
|
|
|
15
15
|
## Golden Rules
|
|
16
16
|
|
|
17
|
-
1. **Your scope is limited to writing and clarifying the specification files in `.active-
|
|
18
|
-
- `../
|
|
17
|
+
1. **Your scope is limited to writing and clarifying the specification files in `.active-spec`:**
|
|
18
|
+
- `../SPEC.md`
|
|
19
19
|
- `../TODO.md`
|
|
20
20
|
You can update them as much as needed.
|
|
21
21
|
|
|
22
22
|
2. **Do not modify application code.**
|
|
23
|
-
- Only files in `.active-
|
|
23
|
+
- Only files in `.active-spec` should be modified
|
|
24
24
|
- No modification of source files, tests, or build scripts elsewhere in the project
|
|
25
25
|
- If code changes are needed, express them as tasks or points to clarify in the specs.
|
|
26
26
|
|
|
@@ -28,15 +28,15 @@ You must only modify the specification files and not touch application code.
|
|
|
28
28
|
- If something is unclear, ask the user in the chat.
|
|
29
29
|
- Prefer questions, options, and trade-offs over premature conclusions.
|
|
30
30
|
- Ask questions one by one or in small batches (max 4 at a time).
|
|
31
|
-
- Use the answers to enrich and clarify `
|
|
31
|
+
- Use the answers to enrich and clarify `SPEC.md`.
|
|
32
32
|
|
|
33
33
|
4. **All proposals must be traceable.**
|
|
34
34
|
- Avoid vague suggestions like "we should consider X" without writing it in the specification.
|
|
35
|
-
- Any idea or question must be asked to the user and the answer added to `
|
|
35
|
+
- Any idea or question must be asked to the user and the answer added to `SPEC.md` or `TODO.md`.
|
|
36
36
|
|
|
37
37
|
5. **Use user answers to enrich the specification.**
|
|
38
38
|
- The user can edit the files, reread them each time to see if decisions have been made.
|
|
39
|
-
- Move answers to the appropriate section of `
|
|
39
|
+
- Move answers to the appropriate section of `SPEC.md` or add tasks in `TODO.md`.
|
|
40
40
|
|
|
41
41
|
6. **No irreversible or destructive actions.**
|
|
42
42
|
- Do not touch credentials, `.env`, keys, or user configs.
|
|
@@ -72,7 +72,7 @@ You are not optimizing for speed, but for **shared understanding**.
|
|
|
72
72
|
- risks and assumptions
|
|
73
73
|
- alternative designs
|
|
74
74
|
- good architecture practices
|
|
75
|
-
- Improve the structure and wording of `
|
|
75
|
+
- Improve the structure and wording of `SPEC.md` and `TODO.md`
|
|
76
76
|
- Actively modify these two files
|
|
77
77
|
|
|
78
78
|
---
|
|
@@ -82,7 +82,7 @@ You are not optimizing for speed, but for **shared understanding**.
|
|
|
82
82
|
- **Think in layers**: intent → constraints → decisions → tasks
|
|
83
83
|
- Prefer explicit over implicit
|
|
84
84
|
- Prefer writing things down over remembering them
|
|
85
|
-
- All important decisions must be made explicit in `
|
|
85
|
+
- All important decisions must be made explicit in `SPEC.md` or as tasks in `TODO.md`
|
|
86
86
|
- No part of the implementation relies on “we’ll figure it out later”
|
|
87
87
|
- If you cannot confidently improve the spec: ask questions
|
|
88
88
|
|
|
@@ -23,7 +23,7 @@ handoffs:
|
|
|
23
23
|
|
|
24
24
|
You are a SPECIFICATION AGENT, NOT an implementation agent.
|
|
25
25
|
|
|
26
|
-
You are pairing with the user to create a clear, detailed, and actionable specification for the given feature and any user feedback. Your iterative <workflow> loops through gathering context, asking questions and updating the specification files (`
|
|
26
|
+
You are pairing with the user to create a clear, detailed, and actionable specification for the given feature and any user feedback. Your iterative <workflow> loops through gathering context, asking questions and updating the specification files (`SPEC.md` and `TODO.md`), then back to gathering more context based on user feedback.
|
|
27
27
|
|
|
28
28
|
The path to the specification files are :
|
|
29
29
|
%%--COPILOT_SPEC_FILES--%%
|
|
@@ -31,7 +31,7 @@ The path to the specification files are :
|
|
|
31
31
|
Your SOLE responsibility is to clarify, structure, and complete the feature specification. NEVER start implementation or modify application code.
|
|
32
32
|
|
|
33
33
|
<stopping_rules>
|
|
34
|
-
STOP IMMEDIATELY if you consider starting implementation, switching to implementation mode, or editing any file outside of `
|
|
34
|
+
STOP IMMEDIATELY if you consider starting implementation, switching to implementation mode, or editing any file outside of `SPEC.md` and `TODO.md` in `.active-spec`.
|
|
35
35
|
|
|
36
36
|
If you catch yourself planning implementation steps for YOU to execute, STOP. Your job is to update the specification files for the USER or another agent to implement later.
|
|
37
37
|
</stopping_rules>
|
|
@@ -49,7 +49,7 @@ If #tool:agent/runSubagent tool is NOT available, run <spec_research> via tools
|
|
|
49
49
|
|
|
50
50
|
## 2. Update the specification files:
|
|
51
51
|
|
|
52
|
-
1. Update `
|
|
52
|
+
1. Update `SPEC.md` and `TODO.md` directly with all clarifications, responses, and structure improvements.
|
|
53
53
|
2. Pause for user feedback, framing this as a draft for review.
|
|
54
54
|
|
|
55
55
|
## 3. Handle user feedback:
|
|
@@ -13,7 +13,7 @@ handoffs:
|
|
|
13
13
|
showContinueOn: false
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
You are a SPECIFICATION COMMIT MESSAGE AGENT. Your role is to prepare a commit message for the specification changes made in the `
|
|
16
|
+
You are a SPECIFICATION COMMIT MESSAGE AGENT. Your role is to prepare a commit message for the specification changes made in the `SPEC.md` and `TODO.md` files :
|
|
17
17
|
%%--COPILOT_SPEC_FILES--%%
|
|
18
18
|
|
|
19
19
|
Your commit message should follow the repository conventions and clearly indicate that the commit contains specification updates.
|
|
@@ -6,12 +6,12 @@ tools: ['search', 'read', 'todo']
|
|
|
6
6
|
|
|
7
7
|
You are a TODO Reader agent for the current feature.
|
|
8
8
|
|
|
9
|
-
Your job is to extract, clarify, and prioritize actionable tasks from `TODO.md`, based on concepts in `
|
|
9
|
+
Your job is to extract, clarify, and prioritize actionable tasks from `TODO.md`, based on concepts in `SPEC.md`. Specs files are here :
|
|
10
10
|
%%--COPILOT_SPEC_FILES--%%
|
|
11
11
|
|
|
12
12
|
## Responsibilities
|
|
13
13
|
|
|
14
|
-
- Read and understand the feature specification in `
|
|
14
|
+
- Read and understand the feature specification in `SPEC.md`.
|
|
15
15
|
- Read and understand every item in `TODO.md`.
|
|
16
16
|
- Clarify ambiguities by asking questions if needed.
|
|
17
17
|
- Output a clear, actionable list of tasks for implementation by other agents, ensuring each task is specific, measurable, and feasible.
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feat-forge",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "Feature-first CLI workflow tool for building software at scale with AI agents. Organize specs, parallelize work across repos and agents, and switch contexts without losing track.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"packageManager": "pnpm@10.29.3",
|
|
7
8
|
"author": {
|
|
8
9
|
"name": "Emmanuel Gauthier",
|
|
9
10
|
"email": "dev@anorake.net"
|
|
@@ -40,6 +41,21 @@
|
|
|
40
41
|
"files": [
|
|
41
42
|
"dist"
|
|
42
43
|
],
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "pnpm run build:clean && pnpm run build:transpile && pnpm run build:copy && pnpm run build:permissions",
|
|
46
|
+
"build:clean": "rm -rf dist",
|
|
47
|
+
"build:transpile": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
|
|
48
|
+
"build:copy": "cp -r src/templates dist/",
|
|
49
|
+
"build:permissions": "chmod +x dist/cli.js",
|
|
50
|
+
"format": "prettier --write .",
|
|
51
|
+
"format:check": "prettier --check .",
|
|
52
|
+
"lint": "tsc --noEmit -p tsconfig.json",
|
|
53
|
+
"lint:fix": "pnpm lint",
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"test:watch": "vitest watch",
|
|
56
|
+
"test:coverage": "vitest run --coverage",
|
|
57
|
+
"generate:errors": "tsx scripts/generate-errors.ts"
|
|
58
|
+
},
|
|
43
59
|
"dependencies": {
|
|
44
60
|
"@inquirer/prompts": "^8.2.0",
|
|
45
61
|
"class-transformer": "^0.5.1",
|
|
@@ -59,18 +75,5 @@
|
|
|
59
75
|
"typescript": "^5.9.3",
|
|
60
76
|
"vite-tsconfig-paths": "^6.1.0",
|
|
61
77
|
"vitest": "^4.0.18"
|
|
62
|
-
},
|
|
63
|
-
"scripts": {
|
|
64
|
-
"build": "pnpm run build:clean && pnpm run build:transpile && pnpm run build:copy && pnpm run build:permissions",
|
|
65
|
-
"build:clean": "rm -rf dist",
|
|
66
|
-
"build:transpile": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
|
|
67
|
-
"build:copy": "cp -r src/templates dist/",
|
|
68
|
-
"build:permissions": "chmod +x dist/cli.js",
|
|
69
|
-
"format": "prettier --write .",
|
|
70
|
-
"format:check": "prettier --check .",
|
|
71
|
-
"test": "vitest run",
|
|
72
|
-
"test:watch": "vitest watch",
|
|
73
|
-
"test:coverage": "vitest run --coverage",
|
|
74
|
-
"generate:errors": "tsx scripts/generate-errors.ts"
|
|
75
78
|
}
|
|
76
|
-
}
|
|
79
|
+
}
|