@aion0/forge 0.4.16 → 0.5.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.
Files changed (92) hide show
  1. package/README.md +1 -1
  2. package/RELEASE_NOTES.md +170 -14
  3. package/app/api/agents/route.ts +17 -0
  4. package/app/api/delivery/[id]/route.ts +62 -0
  5. package/app/api/delivery/route.ts +40 -0
  6. package/app/api/mobile-chat/route.ts +13 -7
  7. package/app/api/monitor/route.ts +10 -6
  8. package/app/api/pipelines/[id]/route.ts +16 -3
  9. package/app/api/tasks/route.ts +2 -1
  10. package/app/api/workspace/[id]/agents/route.ts +35 -0
  11. package/app/api/workspace/[id]/memory/route.ts +23 -0
  12. package/app/api/workspace/[id]/smith/route.ts +22 -0
  13. package/app/api/workspace/[id]/stream/route.ts +28 -0
  14. package/app/api/workspace/route.ts +100 -0
  15. package/app/global-error.tsx +10 -4
  16. package/app/icon.ico +0 -0
  17. package/app/layout.tsx +2 -2
  18. package/app/login/LoginForm.tsx +96 -0
  19. package/app/login/page.tsx +7 -98
  20. package/app/page.tsx +2 -2
  21. package/bin/forge-server.mjs +13 -1
  22. package/check-forge-status.sh +9 -0
  23. package/components/ConversationEditor.tsx +411 -0
  24. package/components/ConversationGraphView.tsx +347 -0
  25. package/components/ConversationTerminalView.tsx +303 -0
  26. package/components/Dashboard.tsx +36 -39
  27. package/components/DashboardWrapper.tsx +9 -0
  28. package/components/DeliveryFlowEditor.tsx +491 -0
  29. package/components/DeliveryList.tsx +230 -0
  30. package/components/DeliveryWorkspace.tsx +589 -0
  31. package/components/DocTerminal.tsx +10 -2
  32. package/components/DocsViewer.tsx +10 -2
  33. package/components/HelpTerminal.tsx +11 -6
  34. package/components/InlinePipelineView.tsx +111 -0
  35. package/components/MobileView.tsx +20 -0
  36. package/components/MonitorPanel.tsx +9 -4
  37. package/components/NewTaskModal.tsx +32 -0
  38. package/components/PipelineEditor.tsx +49 -6
  39. package/components/PipelineView.tsx +482 -64
  40. package/components/ProjectDetail.tsx +314 -56
  41. package/components/ProjectManager.tsx +49 -4
  42. package/components/SessionView.tsx +27 -13
  43. package/components/SettingsModal.tsx +790 -124
  44. package/components/SkillsPanel.tsx +31 -8
  45. package/components/TaskBoard.tsx +3 -0
  46. package/components/WebTerminal.tsx +257 -43
  47. package/components/WorkspaceTree.tsx +221 -0
  48. package/components/WorkspaceView.tsx +2224 -0
  49. package/install.sh +2 -2
  50. package/lib/agents/claude-adapter.ts +104 -0
  51. package/lib/agents/generic-adapter.ts +64 -0
  52. package/lib/agents/index.ts +242 -0
  53. package/lib/agents/types.ts +70 -0
  54. package/lib/artifacts.ts +106 -0
  55. package/lib/delivery.ts +787 -0
  56. package/lib/forge-skills/forge-inbox.md +37 -0
  57. package/lib/forge-skills/forge-send.md +40 -0
  58. package/lib/forge-skills/forge-status.md +32 -0
  59. package/lib/forge-skills/forge-workspace-sync.md +37 -0
  60. package/lib/help-docs/00-overview.md +7 -1
  61. package/lib/help-docs/01-settings.md +159 -2
  62. package/lib/help-docs/05-pipelines.md +89 -0
  63. package/lib/help-docs/07-projects.md +35 -1
  64. package/lib/help-docs/11-workspace.md +204 -0
  65. package/lib/help-docs/CLAUDE.md +5 -2
  66. package/lib/init.ts +60 -10
  67. package/lib/pipeline.ts +537 -1
  68. package/lib/settings.ts +115 -22
  69. package/lib/skills.ts +249 -372
  70. package/lib/task-manager.ts +113 -33
  71. package/lib/telegram-bot.ts +33 -1
  72. package/lib/workspace/__tests__/state-machine.test.ts +388 -0
  73. package/lib/workspace/__tests__/workspace.test.ts +311 -0
  74. package/lib/workspace/agent-bus.ts +416 -0
  75. package/lib/workspace/agent-worker.ts +667 -0
  76. package/lib/workspace/backends/api-backend.ts +262 -0
  77. package/lib/workspace/backends/cli-backend.ts +479 -0
  78. package/lib/workspace/index.ts +82 -0
  79. package/lib/workspace/manager.ts +136 -0
  80. package/lib/workspace/orchestrator.ts +1804 -0
  81. package/lib/workspace/persistence.ts +310 -0
  82. package/lib/workspace/presets.ts +170 -0
  83. package/lib/workspace/skill-installer.ts +188 -0
  84. package/lib/workspace/smith-memory.ts +498 -0
  85. package/lib/workspace/types.ts +231 -0
  86. package/lib/workspace/watch-manager.ts +288 -0
  87. package/lib/workspace-standalone.ts +790 -0
  88. package/middleware.ts +1 -0
  89. package/package.json +4 -1
  90. package/src/config/index.ts +12 -1
  91. package/src/core/db/database.ts +1 -0
  92. package/start.sh +7 -0
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Workspace Persistence — save/load workspace state to disk.
3
+ *
4
+ * Storage layout:
5
+ * ~/.forge/workspaces/{workspace-id}/
6
+ * state.json — workspace config, agent states, node positions, bus log
7
+ * agents/{agent-id}/
8
+ * logs.jsonl — append-only execution log
9
+ * history.json — conversation history snapshot
10
+ */
11
+
12
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, rmSync, renameSync } from 'node:fs';
13
+ import { writeFile, appendFile, mkdir, rename } from 'node:fs/promises';
14
+ import { join } from 'node:path';
15
+ import { homedir } from 'node:os';
16
+ import type { WorkspaceState, AgentState, BusMessage, WorkspaceAgentConfig } from './types';
17
+ import type { TaskLogEntry } from '@/src/types';
18
+
19
+ // ─── Paths ───────────────────────────────────────────────
20
+
21
+ const WORKSPACES_ROOT = join(homedir(), '.forge', 'workspaces');
22
+
23
+ function workspaceDir(workspaceId: string): string {
24
+ return join(WORKSPACES_ROOT, workspaceId);
25
+ }
26
+
27
+ function agentDir(workspaceId: string, agentId: string): string {
28
+ return join(workspaceDir(workspaceId), 'agents', agentId);
29
+ }
30
+
31
+ function stateFile(workspaceId: string): string {
32
+ return join(workspaceDir(workspaceId), 'state.json');
33
+ }
34
+
35
+ function agentLogFile(workspaceId: string, agentId: string): string {
36
+ return join(agentDir(workspaceId, agentId), 'logs.jsonl');
37
+ }
38
+
39
+ function agentHistoryFile(workspaceId: string, agentId: string): string {
40
+ return join(agentDir(workspaceId, agentId), 'history.json');
41
+ }
42
+
43
+ // ─── Save ────────────────────────────────────────────────
44
+
45
+ export async function saveWorkspace(state: WorkspaceState): Promise<void> {
46
+ const dir = workspaceDir(state.id);
47
+ mkdirSync(dir, { recursive: true }); // sync mkdir is fine (fast, cached by OS)
48
+
49
+ const stateToSave: WorkspaceState = {
50
+ ...state,
51
+ agentStates: Object.fromEntries(
52
+ Object.entries(state.agentStates).map(([id, s]) => [id, {
53
+ ...s,
54
+ history: [],
55
+ logFile: agentLogFile(state.id, id),
56
+ }])
57
+ ),
58
+ updatedAt: Date.now(),
59
+ };
60
+
61
+ // Atomic write — write to temp file, then rename (prevents 0-byte on crash)
62
+ const target = stateFile(state.id);
63
+ const tmp = target + '.tmp';
64
+ await writeFile(tmp, JSON.stringify(stateToSave, null, 2), 'utf-8');
65
+ await rename(tmp, target);
66
+
67
+ // Save per-agent history in parallel
68
+ await Promise.all(
69
+ Object.entries(state.agentStates).map(([agentId, agentState]) =>
70
+ saveAgentHistory(state.id, agentId, agentState)
71
+ )
72
+ );
73
+ }
74
+
75
+ async function saveAgentHistory(workspaceId: string, agentId: string, state: AgentState): Promise<void> {
76
+ const dir = agentDir(workspaceId, agentId);
77
+ await mkdir(dir, { recursive: true });
78
+ await writeFile(agentHistoryFile(workspaceId, agentId), JSON.stringify(state.history, null, 2), 'utf-8');
79
+ }
80
+
81
+ /** Synchronous save — used during shutdown to ensure data is written before process exits */
82
+ export function saveWorkspaceSync(state: WorkspaceState): void {
83
+ const dir = workspaceDir(state.id);
84
+ mkdirSync(dir, { recursive: true });
85
+
86
+ const stateToSave: WorkspaceState = {
87
+ ...state,
88
+ agentStates: Object.fromEntries(
89
+ Object.entries(state.agentStates).map(([id, s]) => [id, {
90
+ ...s,
91
+ history: [],
92
+ logFile: agentLogFile(state.id, id),
93
+ }])
94
+ ),
95
+ updatedAt: Date.now(),
96
+ };
97
+
98
+ // Atomic: write temp → rename
99
+ const target = stateFile(state.id);
100
+ const tmp = target + '.tmp';
101
+ writeFileSync(tmp, JSON.stringify(stateToSave, null, 2), 'utf-8');
102
+ renameSync(tmp, target);
103
+
104
+ // Save per-agent history
105
+ for (const [agentId, agentState] of Object.entries(state.agentStates)) {
106
+ const adir = agentDir(state.id, agentId);
107
+ mkdirSync(adir, { recursive: true });
108
+ writeFileSync(agentHistoryFile(state.id, agentId), JSON.stringify(agentState.history, null, 2), 'utf-8');
109
+ }
110
+ }
111
+
112
+ // ─── Append Log ──────────────────────────────────────────
113
+
114
+ /** Append a single log entry to an agent's JSONL log file (async) */
115
+ export async function appendAgentLog(workspaceId: string, agentId: string, entry: TaskLogEntry): Promise<void> {
116
+ const dir = agentDir(workspaceId, agentId);
117
+ await mkdir(dir, { recursive: true });
118
+ await appendFile(agentLogFile(workspaceId, agentId), JSON.stringify(entry) + '\n', 'utf-8');
119
+ }
120
+
121
+ // ─── Load ────────────────────────────────────────────────
122
+
123
+ export function loadWorkspace(workspaceId: string): WorkspaceState | null {
124
+ const file = stateFile(workspaceId);
125
+ if (!existsSync(file)) return null;
126
+
127
+ try {
128
+ const raw = readFileSync(file, 'utf-8');
129
+ if (!raw || raw.trim().length === 0) {
130
+ console.error(`[persistence] Empty state file for workspace ${workspaceId}`);
131
+ return null;
132
+ }
133
+ const state: WorkspaceState = JSON.parse(raw);
134
+
135
+ // Restore per-agent history
136
+ for (const [agentId, agentState] of Object.entries(state.agentStates)) {
137
+ const histFile = agentHistoryFile(workspaceId, agentId);
138
+ if (existsSync(histFile)) {
139
+ try {
140
+ agentState.history = JSON.parse(readFileSync(histFile, 'utf-8'));
141
+ } catch {
142
+ agentState.history = [];
143
+ }
144
+ }
145
+
146
+ // Migrate old status field to new two-layer model
147
+ if ('status' in agentState && !('smithStatus' in agentState)) {
148
+ const oldStatus = (agentState as any).status;
149
+ (agentState as any).smithStatus = 'down';
150
+ (agentState as any).mode = (agentState as any).runMode || 'auto';
151
+ (agentState as any).taskStatus = (oldStatus === 'running' || oldStatus === 'listening') ? 'idle' :
152
+ (oldStatus === 'interrupted') ? 'idle' :
153
+ (oldStatus === 'waiting_approval') ? 'idle' :
154
+ (oldStatus === 'paused') ? 'idle' :
155
+ oldStatus;
156
+ delete (agentState as any).status;
157
+ delete (agentState as any).runMode;
158
+ delete (agentState as any).daemonMode;
159
+ }
160
+ // Mark running agents as idle (they were killed on shutdown)
161
+ if (agentState.taskStatus === 'running') {
162
+ agentState.taskStatus = 'idle';
163
+ }
164
+ }
165
+
166
+ // Migrate Input nodes: content → entries
167
+ for (const agent of state.agents) {
168
+ if (agent.type === 'input') {
169
+ if (!agent.entries) agent.entries = [];
170
+ if (agent.entries.length === 0 && agent.content) {
171
+ agent.entries.push({ content: agent.content, timestamp: state.updatedAt || Date.now() });
172
+ }
173
+ }
174
+ }
175
+
176
+ return state;
177
+ } catch {
178
+ return null;
179
+ }
180
+ }
181
+
182
+ // ─── List ────────────────────────────────────────────────
183
+
184
+ export interface WorkspaceSummary {
185
+ id: string;
186
+ projectPath: string;
187
+ projectName: string;
188
+ agentCount: number;
189
+ createdAt: number;
190
+ updatedAt: number;
191
+ }
192
+
193
+ export function listWorkspaces(): WorkspaceSummary[] {
194
+ if (!existsSync(WORKSPACES_ROOT)) return [];
195
+
196
+ const results: WorkspaceSummary[] = [];
197
+
198
+ for (const entry of readdirSync(WORKSPACES_ROOT, { withFileTypes: true })) {
199
+ if (!entry.isDirectory()) continue;
200
+ const file = stateFile(entry.name);
201
+ if (!existsSync(file)) continue;
202
+
203
+ try {
204
+ const raw = readFileSync(file, 'utf-8');
205
+ const state: WorkspaceState = JSON.parse(raw);
206
+ results.push({
207
+ id: state.id,
208
+ projectPath: state.projectPath,
209
+ projectName: state.projectName,
210
+ agentCount: state.agents.length,
211
+ createdAt: state.createdAt,
212
+ updatedAt: state.updatedAt,
213
+ });
214
+ } catch {
215
+ // Skip corrupted state files
216
+ }
217
+ }
218
+
219
+ return results.sort((a, b) => b.updatedAt - a.updatedAt);
220
+ }
221
+
222
+ /** Find workspace by project path */
223
+ export function findWorkspaceByProject(projectPath: string): WorkspaceState | null {
224
+ if (!existsSync(WORKSPACES_ROOT)) return null;
225
+
226
+ for (const entry of readdirSync(WORKSPACES_ROOT, { withFileTypes: true })) {
227
+ if (!entry.isDirectory()) continue;
228
+ const file = stateFile(entry.name);
229
+ if (!existsSync(file)) continue;
230
+
231
+ try {
232
+ const raw = readFileSync(file, 'utf-8');
233
+ const state: WorkspaceState = JSON.parse(raw);
234
+ if (state.projectPath === projectPath) {
235
+ return loadWorkspace(state.id);
236
+ }
237
+ } catch {
238
+ continue;
239
+ }
240
+ }
241
+
242
+ return null;
243
+ }
244
+
245
+ // ─── Delete ──────────────────────────────────────────────
246
+
247
+ export function deleteWorkspace(workspaceId: string): boolean {
248
+ const dir = workspaceDir(workspaceId);
249
+ if (!existsSync(dir)) return false;
250
+ rmSync(dir, { recursive: true, force: true });
251
+ return true;
252
+ }
253
+
254
+ // ─── Read Agent Logs ─────────────────────────────────────
255
+
256
+ /** Read the full JSONL log for an agent */
257
+ export function readAgentLog(workspaceId: string, agentId: string): TaskLogEntry[] {
258
+ const file = agentLogFile(workspaceId, agentId);
259
+ if (!existsSync(file)) return [];
260
+
261
+ try {
262
+ const raw = readFileSync(file, 'utf-8');
263
+ return raw.split('\n').filter(Boolean).map(line => {
264
+ try { return JSON.parse(line); } catch { return null; }
265
+ }).filter(Boolean) as TaskLogEntry[];
266
+ } catch {
267
+ return [];
268
+ }
269
+ }
270
+
271
+ /** Read the last N log entries for an agent */
272
+ export function readAgentLogTail(workspaceId: string, agentId: string, n = 20): TaskLogEntry[] {
273
+ const log = readAgentLog(workspaceId, agentId);
274
+ return log.slice(-n);
275
+ }
276
+
277
+ /** Clear agent log file */
278
+ export function clearAgentLog(workspaceId: string, agentId: string): void {
279
+ const file = agentLogFile(workspaceId, agentId);
280
+ if (existsSync(file)) writeFileSync(file, '');
281
+ }
282
+
283
+ // ─── Auto-save timer ─────────────────────────────────────
284
+
285
+ const saveTimers = new Map<string, NodeJS.Timeout>();
286
+
287
+ /**
288
+ * Start periodic auto-save for a workspace.
289
+ * Calls `getState()` to get current state and saves it.
290
+ */
291
+ export function startAutoSave(workspaceId: string, getState: () => WorkspaceState, intervalMs = 10_000): void {
292
+ stopAutoSave(workspaceId);
293
+ const timer = setInterval(() => {
294
+ try {
295
+ const state = getState();
296
+ saveWorkspace(state).catch(() => {});
297
+ } catch {
298
+ // Silently ignore save errors
299
+ }
300
+ }, intervalMs);
301
+ saveTimers.set(workspaceId, timer);
302
+ }
303
+
304
+ export function stopAutoSave(workspaceId: string): void {
305
+ const timer = saveTimers.get(workspaceId);
306
+ if (timer) {
307
+ clearInterval(timer);
308
+ saveTimers.delete(workspaceId);
309
+ }
310
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Preset agent templates — default roles with predefined steps.
3
+ *
4
+ * Directory conventions:
5
+ * docs/prd/ — PM output (versioned PRD files)
6
+ * docs/architecture/ — Engineer design docs
7
+ * docs/qa/ — QA test plans and reports
8
+ * docs/review/ — Reviewer reports
9
+ * src/ — Engineer implementation
10
+ * tests/ — QA test code
11
+ */
12
+
13
+ import type { WorkspaceAgentConfig } from './types';
14
+
15
+ type PresetTemplate = Omit<WorkspaceAgentConfig, 'id'>;
16
+
17
+ export const AGENT_PRESETS: Record<string, PresetTemplate> = {
18
+ pm: {
19
+ label: 'PM',
20
+ icon: '📋',
21
+ role: `You are a Product Manager. Your output goes in docs/prd/ directory.
22
+
23
+ Rules:
24
+ - Each PRD version is a separate file: docs/prd/v1.0-initial.md, docs/prd/v1.1-add-history.md, etc.
25
+ - NEVER overwrite existing PRD files. Always create a new version file.
26
+ - The version number should reflect the scope: patch (v1.0.1) for small fixes, minor (v1.1) for new features, major (v2.0) for major changes.
27
+ - Each file should reference which requirements it addresses.
28
+ - Do NOT write code or modify source files.`,
29
+ backend: 'cli',
30
+ agentId: 'claude',
31
+ dependsOn: [],
32
+ workDir: './',
33
+ outputs: ['docs/prd/'],
34
+ steps: [
35
+ { id: 'analyze', label: 'Analyze Requirements', prompt: 'Read the new requirements from upstream input. Then list all existing files in docs/prd/ to understand what versions exist. Identify what is NEW vs what was already covered in previous PRD versions.' },
36
+ { id: 'write-prd', label: 'Write PRD', prompt: 'Create a NEW versioned PRD file in docs/prd/ (e.g., docs/prd/v1.1-feature-name.md). Include: version, date, referenced requirements, goals, user stories, acceptance criteria, and technical constraints. Do NOT overwrite any existing PRD file.' },
37
+ { id: 'review', label: 'Self-Review', prompt: 'Review the PRD you just wrote. Ensure version number is correct, all new requirements are covered, and it does not duplicate content from previous versions. Fix any issues.' },
38
+ ],
39
+ },
40
+
41
+ engineer: {
42
+ label: 'Engineer',
43
+ icon: '🔨',
44
+ role: `You are a Senior Software Engineer. Your design docs go in docs/architecture/ directory.
45
+
46
+ Rules:
47
+ - Read ALL files in docs/prd/ to understand the full requirements history.
48
+ - Read ALL files in docs/architecture/ to understand previous design decisions.
49
+ - Only implement NEW or CHANGED requirements. Check your memory and existing code first.
50
+ - Architecture docs are versioned: docs/architecture/v1.0-initial.md, etc.
51
+ - Do NOT rewrite existing working code unless the PRD explicitly requires changes.`,
52
+ backend: 'cli',
53
+ agentId: 'claude',
54
+ dependsOn: [],
55
+ workDir: './',
56
+ outputs: ['src/', 'docs/architecture/'],
57
+ steps: [
58
+ { id: 'design', label: 'Architecture Design', prompt: 'Read all files in docs/prd/ (latest first) and docs/architecture/. Identify what needs to be designed or changed. Create a new architecture doc in docs/architecture/ (e.g., docs/architecture/v1.1-add-history.md) describing the changes. Do NOT overwrite existing architecture files.' },
59
+ { id: 'implement', label: 'Implementation', prompt: 'Implement the features based on your architecture design. Only modify files that need to change. Write clean, well-documented code.' },
60
+ { id: 'self-test', label: 'Self-Test', prompt: 'Review your implementation. Run any existing tests. Fix any obvious issues.' },
61
+ ],
62
+ },
63
+
64
+ qa: {
65
+ label: 'QA',
66
+ icon: '🧪',
67
+ role: `You are a QA Engineer. Your test plans and reports go in docs/qa/ directory.
68
+
69
+ Rules:
70
+ - Read docs/prd/ to understand requirements (focus on latest version).
71
+ - Read docs/qa/ to see what was already tested. Skip tests that already passed for unchanged features.
72
+ - Test plans are versioned: docs/qa/test-plan-v1.1.md
73
+ - Test reports are versioned: docs/qa/test-report-v1.1.md
74
+ - Test code goes in tests/ directory.
75
+ - Do NOT fix bugs — only report them clearly in your test report.
76
+
77
+ Communication rules:
78
+ - Only send [SEND:...] messages for BLOCKING issues that prevent the product from working.
79
+ - Minor issues, suggestions, and style feedback go in your test report ONLY — do NOT send messages for these.
80
+ - Send at most 1-2 messages total. Consolidate multiple issues into one message.
81
+ - Never send messages during Test Planning or Write Tests steps — only in Execute Tests.`,
82
+ backend: 'cli',
83
+ agentId: 'claude',
84
+ dependsOn: [],
85
+ workDir: './',
86
+ outputs: ['tests/', 'docs/qa/'],
87
+ steps: [
88
+ { id: 'plan', label: 'Test Planning', prompt: 'Read the latest PRD in docs/prd/ and existing test plans in docs/qa/. Write a NEW test plan file covering only the NEW/CHANGED features.' },
89
+ { id: 'write-tests', label: 'Write Tests', prompt: 'Implement test cases in tests/ directory based on your test plan. Add new tests, do not rewrite existing passing tests. Do NOT send any messages to other agents in this step.' },
90
+ { id: 'execute', label: 'Execute Tests', prompt: 'Run all tests. Write a test report documenting results. Only if you find BLOCKING bugs (app crashes, data loss, security holes), send ONE consolidated message: [SEND:Engineer:fix_request] followed by a brief list of blocking issues. Minor issues go in the report only.' },
91
+ ],
92
+ },
93
+
94
+ reviewer: {
95
+ label: 'Reviewer',
96
+ icon: '🔍',
97
+ role: `You are a Code Reviewer. Your review reports go in docs/review/ directory.
98
+
99
+ Rules:
100
+ - Read docs/prd/ (latest) to understand what should have been implemented.
101
+ - Read docs/architecture/ (latest) to understand design decisions.
102
+ - Review ONLY recent code changes, not the entire codebase.
103
+ - Review reports are versioned: docs/review/review-v1.1.md
104
+ - Do NOT modify code directly.
105
+
106
+ Communication rules:
107
+ - Only send [SEND:...] messages for CRITICAL issues: security vulnerabilities, data corruption, or completely broken functionality.
108
+ - All other feedback (code style, performance suggestions, minor issues) goes in your review report ONLY.
109
+ - Send at most 1 message to Engineer and 1 to PM. Consolidate issues.
110
+ - If no critical issues found, do NOT send any messages.`,
111
+ backend: 'cli',
112
+ agentId: 'claude',
113
+ dependsOn: [],
114
+ workDir: './',
115
+ outputs: ['docs/review/'],
116
+ steps: [
117
+ { id: 'review-code', label: 'Code Review', prompt: 'Read the latest PRD and architecture docs. Review recent source code changes. Check for: code quality, security issues, performance, naming conventions, and adherence to PRD.' },
118
+ { id: 'report', label: 'Write Report', prompt: 'Write a review report with all findings. Only if you found CRITICAL issues (security, data corruption, broken core functionality), send ONE consolidated message: [SEND:Engineer:fix_request] with the critical issues. For requirement problems: [SEND:PM:fix_request]. If no critical issues, do NOT send any messages.' },
119
+ ],
120
+ },
121
+ };
122
+
123
+ /**
124
+ * Create a full dev pipeline: Input → PM → Engineer → QA → Reviewer
125
+ * With proper dependsOn wiring, versioned output directories, and incremental prompts.
126
+ */
127
+ export function createDevPipeline(): WorkspaceAgentConfig[] {
128
+ const ts = Date.now();
129
+ const inputId = `input-${ts}`;
130
+ const pmId = `pm-${ts}`;
131
+ const engId = `engineer-${ts}`;
132
+ const qaId = `qa-${ts}`;
133
+ const revId = `reviewer-${ts}`;
134
+
135
+ return [
136
+ {
137
+ id: inputId, label: 'Requirements', icon: '📝',
138
+ type: 'input', content: '', entries: [],
139
+ role: '', backend: 'cli', dependsOn: [], outputs: [], steps: [],
140
+ },
141
+ {
142
+ ...AGENT_PRESETS.pm, id: pmId, dependsOn: [inputId],
143
+ },
144
+ {
145
+ ...AGENT_PRESETS.engineer, id: engId, dependsOn: [pmId],
146
+ },
147
+ {
148
+ ...AGENT_PRESETS.qa, id: qaId, dependsOn: [engId],
149
+ },
150
+ {
151
+ ...AGENT_PRESETS.reviewer, id: revId, dependsOn: [engId, qaId],
152
+ },
153
+ ];
154
+ }
155
+
156
+ /** @deprecated Use createDevPipeline instead */
157
+ export function createDeliveryPipeline(): WorkspaceAgentConfig[] {
158
+ return createDevPipeline();
159
+ }
160
+
161
+ /** Get a preset by key, assigning a unique ID */
162
+ export function createFromPreset(key: string, overrides?: Partial<WorkspaceAgentConfig>): WorkspaceAgentConfig {
163
+ const preset = AGENT_PRESETS[key];
164
+ if (!preset) throw new Error(`Unknown preset: ${key}`);
165
+ return {
166
+ ...preset,
167
+ id: `${key}-${Date.now()}`,
168
+ ...overrides,
169
+ };
170
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Forge Skills Auto-Installer — installs forge skills into user's ~/.claude/skills/
3
+ * so they are available across all projects and sessions.
4
+ */
5
+
6
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'node:fs';
7
+ import { join, dirname } from 'node:path';
8
+ import { homedir } from 'node:os';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const _filename = typeof __filename !== 'undefined' ? __filename : fileURLToPath(import.meta.url);
12
+ const _dirname = typeof __dirname !== 'undefined' ? __dirname : dirname(_filename);
13
+ const FORGE_SKILLS_DIR = join(_dirname, '..', 'forge-skills');
14
+
15
+ /**
16
+ * Install forge workspace skills into user's ~/.claude/skills/.
17
+ * Skills use env vars ($FORGE_PORT, $FORGE_WORKSPACE_ID, $FORGE_AGENT_ID)
18
+ * so they work across all projects without per-project configuration.
19
+ */
20
+ export function installForgeSkills(
21
+ projectPath: string,
22
+ workspaceId: string,
23
+ agentId: string,
24
+ forgePort = 8403,
25
+ ): { installed: string[] } {
26
+ const skillsDir = join(homedir(), '.claude', 'skills');
27
+ mkdirSync(skillsDir, { recursive: true });
28
+
29
+ const installed: string[] = [];
30
+
31
+ // Read all skill templates
32
+ let sourceDir = FORGE_SKILLS_DIR;
33
+ if (!existsSync(sourceDir)) {
34
+ sourceDir = join(process.cwd(), 'lib', 'forge-skills');
35
+ }
36
+ if (!existsSync(sourceDir)) return { installed };
37
+
38
+ const files = readdirSync(sourceDir).filter(f => f.endsWith('.md'));
39
+
40
+ for (const file of files) {
41
+ const content = readFileSync(join(sourceDir, file), 'utf-8');
42
+ // Claude Code expects skills as directories with SKILL.md inside
43
+ const skillName = file.replace('.md', '');
44
+ const skillDir = join(skillsDir, skillName);
45
+ mkdirSync(skillDir, { recursive: true });
46
+ writeFileSync(join(skillDir, 'SKILL.md'), content, 'utf-8');
47
+ installed.push(skillName);
48
+ }
49
+
50
+ // Clean up old flat .md files (from previous install format)
51
+ for (const file of files) {
52
+ const flatFile = join(skillsDir, file);
53
+ if (existsSync(flatFile)) {
54
+ try { require('node:fs').unlinkSync(flatFile); } catch {}
55
+ }
56
+ }
57
+
58
+ // Ensure settings allow forge curl commands (check both global and project)
59
+ ensureForgePermissions(join(homedir(), '.claude'));
60
+ // Also fix project-level deny rules that might block forge curl
61
+ const projectClaudeDir = join(projectPath, '.claude');
62
+ if (existsSync(join(projectClaudeDir, 'settings.json'))) {
63
+ ensureForgePermissions(projectClaudeDir);
64
+ }
65
+
66
+ return { installed };
67
+ }
68
+
69
+ /**
70
+ * Ensure project's .claude/settings.json allows forge skill curl commands.
71
+ * Removes curl deny rules that block forge, adds allow rule if needed.
72
+ */
73
+ function ensureForgePermissions(projectPath: string): void {
74
+ const settingsFile = join(projectPath, '.claude', 'settings.json');
75
+ const FORGE_CURL_ALLOW = 'Bash(curl*localhost*/smith*)';
76
+
77
+ try {
78
+ let settings: any = {};
79
+ if (existsSync(settingsFile)) {
80
+ settings = JSON.parse(readFileSync(settingsFile, 'utf-8'));
81
+ }
82
+
83
+ if (!settings.permissions) settings.permissions = {};
84
+ if (!settings.permissions.allow) settings.permissions.allow = [];
85
+ if (!settings.permissions.deny) settings.permissions.deny = [];
86
+
87
+ let changed = false;
88
+
89
+ // Remove deny rules that block curl to localhost (forge skills)
90
+ const denyBefore = settings.permissions.deny.length;
91
+ settings.permissions.deny = settings.permissions.deny.filter((rule: string) => {
92
+ // Remove broad curl denies that would block forge
93
+ if (/^Bash\(curl[:\s*]/.test(rule)) return false;
94
+ return true;
95
+ });
96
+ if (settings.permissions.deny.length !== denyBefore) changed = true;
97
+
98
+ // Add forge curl allow if not present
99
+ const hasForgeAllow = settings.permissions.allow.some((rule: string) =>
100
+ rule.includes('localhost') && rule.includes('smith')
101
+ );
102
+ if (!hasForgeAllow) {
103
+ settings.permissions.allow.push(FORGE_CURL_ALLOW);
104
+ changed = true;
105
+ }
106
+
107
+ if (changed) {
108
+ mkdirSync(join(projectPath, '.claude'), { recursive: true });
109
+ writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
110
+ console.log('[skills] Updated .claude/settings.json: allowed forge curl commands');
111
+ }
112
+ } catch (err: any) {
113
+ console.error('[skills] Failed to update .claude/settings.json:', err.message);
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Apply agent profile config to project's .claude/settings.json.
119
+ * Sets env vars and model from the profile so interactive claude uses the right config.
120
+ */
121
+ export function applyProfileToProject(
122
+ projectPath: string,
123
+ profile: { env?: Record<string, string>; model?: string },
124
+ ): void {
125
+ if (!profile.env && !profile.model) return;
126
+
127
+ const settingsFile = join(projectPath, '.claude', 'settings.json');
128
+ try {
129
+ let settings: any = {};
130
+ if (existsSync(settingsFile)) {
131
+ settings = JSON.parse(readFileSync(settingsFile, 'utf-8'));
132
+ }
133
+
134
+ let changed = false;
135
+
136
+ // Set env vars from profile
137
+ if (profile.env && Object.keys(profile.env).length > 0) {
138
+ if (!settings.env) settings.env = {};
139
+ for (const [key, value] of Object.entries(profile.env)) {
140
+ if (settings.env[key] !== value) {
141
+ settings.env[key] = value;
142
+ changed = true;
143
+ }
144
+ }
145
+ }
146
+
147
+ // Set model from profile
148
+ if (profile.model) {
149
+ if (settings.model !== profile.model) {
150
+ settings.model = profile.model;
151
+ changed = true;
152
+ }
153
+ }
154
+
155
+ if (changed) {
156
+ mkdirSync(join(projectPath, '.claude'), { recursive: true });
157
+ writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
158
+ console.log(`[skills] Applied profile config to .claude/settings.json (model=${profile.model || 'default'}, env=${Object.keys(profile.env || {}).length} vars)`);
159
+ }
160
+ } catch (err: any) {
161
+ console.error('[skills] Failed to apply profile config:', err.message);
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Check if forge skills are already installed for this agent.
167
+ */
168
+ export function hasForgeSkills(projectPath: string): boolean {
169
+ const globalDir = join(homedir(), '.claude', 'skills');
170
+ return existsSync(join(globalDir, 'forge-workspace-sync', 'SKILL.md'));
171
+ }
172
+
173
+ /**
174
+ * Remove forge skills from a project.
175
+ */
176
+ export function removeForgeSkills(projectPath: string): void {
177
+ const skillsDir = join(homedir(), '.claude', 'skills');
178
+ if (!existsSync(skillsDir)) return;
179
+
180
+ const forgeSkills = readdirSync(skillsDir).filter(f => f.startsWith('forge-'));
181
+ for (const name of forgeSkills) {
182
+ const p = join(skillsDir, name);
183
+ try {
184
+ const { rmSync } = require('node:fs');
185
+ rmSync(p, { recursive: true, force: true });
186
+ } catch {}
187
+ }
188
+ }