@aion0/forge 0.4.2 → 0.4.4

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.
@@ -64,15 +64,13 @@ export default memo(function ProjectDetail({ projectPath, projectName, hasGit }:
64
64
  const [diffFile, setDiffFile] = useState<string | null>(null);
65
65
  const [projectSkills, setProjectSkills] = useState<{ name: string; displayName: string; type: string; scope: string; version: string; installedVersion: string; hasUpdate: boolean; source: 'registry' | 'local' }[]>([]);
66
66
  const [showSkillsDetail, setShowSkillsDetail] = useState(false);
67
- const [projectTab, setProjectTab] = useState<'code' | 'skills' | 'claudemd' | 'issues'>('code');
68
- // Issue autofix state
69
- const [issueConfig, setIssueConfig] = useState<{ enabled: boolean; interval: number; labels: string[]; baseBranch: string } | null>(null);
70
- const [issueProcessed, setIssueProcessed] = useState<{ issueNumber: number; pipelineId: string; prNumber: number | null; status: string; createdAt: string }[]>([]);
71
- const [issueScanning, setIssueScanning] = useState(false);
72
- const [issueManualId, setIssueManualId] = useState('');
73
- const [issueNextScan, setIssueNextScan] = useState<string | null>(null);
74
- const [issueLastScan, setIssueLastScan] = useState<string | null>(null);
75
- const [retryModal, setRetryModal] = useState<{ issueNumber: number; context: string } | null>(null);
67
+ const [projectTab, setProjectTab] = useState<'code' | 'skills' | 'claudemd' | 'pipelines'>('code');
68
+ // Pipeline bindings state
69
+ const [pipelineBindings, setPipelineBindings] = useState<{ id: number; workflowName: string; enabled: boolean; config: any; lastRunAt: string | null; nextRunAt: string | null }[]>([]);
70
+ const [pipelineRuns, setPipelineRuns] = useState<{ id: string; workflowName: string; pipelineId: string; status: string; summary: string; createdAt: string }[]>([]);
71
+ const [availableWorkflows, setAvailableWorkflows] = useState<{ name: string; description?: string; builtin?: boolean }[]>([]);
72
+ const [showAddPipeline, setShowAddPipeline] = useState(false);
73
+ const [triggerInput, setTriggerInput] = useState<Record<string, string>>({});
76
74
  const [claudeMdContent, setClaudeMdContent] = useState('');
77
75
  const [claudeMdExists, setClaudeMdExists] = useState(false);
78
76
  const [claudeTemplates, setClaudeTemplates] = useState<{ id: string; name: string; description: string; tags: string[]; builtin: boolean; content: string }[]>([]);
@@ -220,57 +218,28 @@ export default memo(function ProjectDetail({ projectPath, projectName, hasGit }:
220
218
  fetchProjectSkills();
221
219
  };
222
220
 
223
- const fetchIssueConfig = useCallback(async () => {
221
+ const fetchPipelineBindings = useCallback(async () => {
224
222
  try {
225
- const res = await fetch(`/api/issue-scanner?project=${encodeURIComponent(projectPath)}`);
223
+ const res = await fetch(`/api/project-pipelines?project=${encodeURIComponent(projectPath)}`);
224
+ if (!res.ok) return;
226
225
  const data = await res.json();
227
- setIssueConfig(data.config || { enabled: false, interval: 30, labels: [], baseBranch: '' });
228
- setIssueProcessed(data.processed || []);
229
- setIssueLastScan(data.lastScan || null);
230
- setIssueNextScan(data.nextScan || null);
226
+ setPipelineBindings(data.bindings || []);
227
+ setPipelineRuns(data.runs || []);
228
+ setAvailableWorkflows(data.workflows || []);
231
229
  } catch {}
232
230
  }, [projectPath]);
233
231
 
234
- const saveIssueConfig = async (config: any) => {
235
- await fetch('/api/issue-scanner', {
236
- method: 'POST',
237
- headers: { 'Content-Type': 'application/json' },
238
- body: JSON.stringify({ action: 'save-config', projectPath, projectName, ...config }),
239
- });
240
- fetchIssueConfig();
241
- };
242
-
243
- const scanNow = async () => {
244
- setIssueScanning(true);
232
+ const triggerProjectPipeline = async (workflowName: string, input?: Record<string, string>) => {
245
233
  try {
246
- const res = await fetch('/api/issue-scanner', {
234
+ const res = await fetch('/api/project-pipelines', {
247
235
  method: 'POST',
248
236
  headers: { 'Content-Type': 'application/json' },
249
- body: JSON.stringify({ action: 'scan', projectPath }),
237
+ body: JSON.stringify({ action: 'trigger', projectPath, projectName, workflowName, input }),
250
238
  });
251
239
  const data = await res.json();
252
- if (data.error) {
253
- alert(data.error);
254
- } else if (data.triggered > 0) {
255
- alert(`Triggered ${data.triggered} issue fix(es): #${data.issues.join(', #')}`);
256
- } else {
257
- alert(`Scanned ${data.total} open issues — no new issues to process`);
258
- }
259
- await fetchIssueConfig();
260
- } catch (e) {
261
- alert('Scan failed');
262
- }
263
- setIssueScanning(false);
264
- };
265
-
266
- const triggerIssue = async (issueId: string) => {
267
- await fetch('/api/issue-scanner', {
268
- method: 'POST',
269
- headers: { 'Content-Type': 'application/json' },
270
- body: JSON.stringify({ action: 'trigger', projectPath, issueId, projectName }),
271
- });
272
- setIssueManualId('');
273
- fetchIssueConfig();
240
+ if (data.ok) { fetchPipelineBindings(); }
241
+ else { alert(data.error || 'Failed'); }
242
+ } catch { alert('Failed to trigger pipeline'); }
274
243
  };
275
244
 
276
245
  const fetchClaudeMd = useCallback(async () => {
@@ -409,9 +378,9 @@ export default memo(function ProjectDetail({ projectPath, projectName, hasGit }:
409
378
  // Lazy load tab-specific data only when switching to that tab
410
379
  useEffect(() => {
411
380
  if (projectTab === 'skills') fetchProjectSkills();
412
- if (projectTab === 'issues') fetchIssueConfig();
381
+ if (projectTab === 'pipelines') fetchPipelineBindings();
413
382
  if (projectTab === 'claudemd') fetchClaudeMd();
414
- }, [projectTab, fetchProjectSkills, fetchIssueConfig, fetchClaudeMd]);
383
+ }, [projectTab, fetchProjectSkills, fetchPipelineBindings, fetchClaudeMd]);
415
384
 
416
385
  return (
417
386
  <>
@@ -481,13 +450,13 @@ export default memo(function ProjectDetail({ projectPath, projectName, hasGit }:
481
450
  {claudeMdExists && <span className="ml-1 text-[8px] text-[var(--green)]">•</span>}
482
451
  </button>
483
452
  <button
484
- onClick={() => setProjectTab('issues')}
453
+ onClick={() => setProjectTab('pipelines')}
485
454
  className={`text-[9px] px-2 py-0.5 rounded transition-colors ${
486
- projectTab === 'issues' ? 'bg-[var(--bg-secondary)] text-[var(--text-primary)] shadow-sm' : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
455
+ projectTab === 'pipelines' ? 'bg-[var(--bg-secondary)] text-[var(--text-primary)] shadow-sm' : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
487
456
  }`}
488
457
  >
489
- Issues
490
- {issueConfig?.enabled && <span className="ml-1 text-[8px] text-[var(--green)]">•</span>}
458
+ Pipelines
459
+ {pipelineBindings.length > 0 && <span className="ml-1 text-[8px] text-[var(--text-secondary)]">({pipelineBindings.length})</span>}
491
460
  </button>
492
461
  </div>
493
462
  </div>
@@ -797,149 +766,157 @@ export default memo(function ProjectDetail({ projectPath, projectName, hasGit }:
797
766
  </div>
798
767
  )}
799
768
 
800
- {/* Issues tab */}
801
- {projectTab === 'issues' && issueConfig && (
769
+ {/* Pipelines tab */}
770
+ {projectTab === 'pipelines' && (
802
771
  <div className="flex-1 overflow-auto p-4 space-y-4">
803
- {/* Config */}
772
+ {/* Bound workflows */}
804
773
  <div className="space-y-3">
805
- <div className="flex items-center gap-3">
806
- <label className="flex items-center gap-2 cursor-pointer">
807
- <input
808
- type="checkbox"
809
- checked={issueConfig.enabled}
810
- onChange={e => setIssueConfig({ ...issueConfig, enabled: e.target.checked })}
811
- className="accent-[var(--accent)]"
812
- />
813
- <span className="text-[11px] text-[var(--text-primary)] font-semibold">Enable Issue Auto-fix</span>
814
- </label>
815
- {issueConfig.enabled && (<>
816
- <button
817
- onClick={() => scanNow()}
818
- disabled={issueScanning}
819
- className="text-[9px] px-2 py-0.5 border border-[var(--accent)] text-[var(--accent)] rounded hover:bg-[var(--accent)] hover:text-white disabled:opacity-50"
820
- >
821
- {issueScanning ? 'Scanning...' : 'Scan Now'}
822
- </button>
823
- {issueLastScan && (
824
- <span className="text-[8px] text-[var(--text-secondary)]">
825
- Last: {new Date(issueLastScan).toLocaleTimeString()}
826
- </span>
827
- )}
828
- {issueNextScan && (
829
- <span className="text-[8px] text-[var(--text-secondary)]">
830
- Next: {new Date(issueNextScan).toLocaleTimeString()}
831
- </span>
832
- )}
833
- </> )}
774
+ <div className="flex items-center gap-2">
775
+ <span className="text-xs font-semibold text-[var(--text-primary)]">Bound Pipelines</span>
776
+ <button
777
+ onClick={() => setShowAddPipeline(v => !v)}
778
+ className="text-[9px] px-2 py-0.5 bg-[var(--accent)] text-white rounded hover:opacity-90 ml-auto"
779
+ >+ Add</button>
834
780
  </div>
835
781
 
836
- {issueConfig.enabled && (
837
- <div className="grid grid-cols-2 gap-3">
838
- <div>
839
- <label className="text-[9px] text-[var(--text-secondary)] block mb-1">Scan Interval (minutes, 0=manual)</label>
840
- <input
841
- type="number"
842
- value={issueConfig.interval}
843
- onChange={e => setIssueConfig({ ...issueConfig, interval: parseInt(e.target.value) || 0 })}
844
- className="w-full px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-[10px] text-[var(--text-primary)]"
845
- />
846
- </div>
847
- <div>
848
- <label className="text-[9px] text-[var(--text-secondary)] block mb-1">Base Branch (empty=auto)</label>
849
- <input
850
- type="text"
851
- value={issueConfig.baseBranch}
852
- onChange={e => setIssueConfig({ ...issueConfig, baseBranch: e.target.value })}
853
- placeholder="main"
854
- className="w-full px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-[10px] text-[var(--text-primary)]"
855
- />
856
- </div>
857
- <div className="col-span-2">
858
- <label className="text-[9px] text-[var(--text-secondary)] block mb-1">Labels Filter (comma-separated, empty=all)</label>
859
- <input
860
- type="text"
861
- value={issueConfig.labels.join(', ')}
862
- onChange={e => setIssueConfig({ ...issueConfig, labels: e.target.value.split(',').map(s => s.trim()).filter(Boolean) })}
863
- placeholder="bug, fix"
864
- className="w-full px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-[10px] text-[var(--text-primary)]"
865
- />
866
- </div>
782
+ {/* Add pipeline form */}
783
+ {showAddPipeline && (
784
+ <div className="border border-[var(--border)] rounded p-2 space-y-2">
785
+ {availableWorkflows.filter(w => !pipelineBindings.find(b => b.workflowName === w.name)).map(w => (
786
+ <button
787
+ key={w.name}
788
+ onClick={async () => {
789
+ await fetch('/api/project-pipelines', {
790
+ method: 'POST',
791
+ headers: { 'Content-Type': 'application/json' },
792
+ body: JSON.stringify({ action: 'add', projectPath, projectName, workflowName: w.name }),
793
+ });
794
+ setShowAddPipeline(false);
795
+ fetchPipelineBindings();
796
+ }}
797
+ className="w-full text-left px-2 py-1.5 rounded hover:bg-[var(--bg-tertiary)] text-[10px] flex items-center gap-2"
798
+ >
799
+ {w.builtin && <span className="text-[7px] text-[var(--text-secondary)]">⚙</span>}
800
+ <span className="text-[var(--text-primary)]">{w.name}</span>
801
+ {w.description && <span className="text-[var(--text-secondary)] truncate ml-auto text-[8px]">{w.description}</span>}
802
+ </button>
803
+ ))}
804
+ {availableWorkflows.filter(w => !pipelineBindings.find(b => b.workflowName === w.name)).length === 0 && (
805
+ <p className="text-[9px] text-[var(--text-secondary)] p-2">All workflows already bound</p>
806
+ )}
867
807
  </div>
868
808
  )}
869
- <div className="mt-3">
870
- <button
871
- onClick={() => saveIssueConfig(issueConfig)}
872
- className="text-[10px] px-4 py-1.5 bg-[var(--accent)] text-white rounded hover:opacity-90"
873
- >Save Configuration</button>
874
- </div>
875
- </div>
876
809
 
877
- {/* Manual trigger */}
878
- <div className="border-t border-[var(--border)] pt-3">
879
- <div className="text-[9px] text-[var(--text-secondary)] uppercase mb-2">Manual Trigger</div>
880
- <div className="flex gap-2">
881
- <input
882
- type="text"
883
- value={issueManualId}
884
- onChange={e => setIssueManualId(e.target.value)}
885
- placeholder="Issue #"
886
- className="w-24 px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-[10px] text-[var(--text-primary)]"
887
- />
888
- <button
889
- onClick={() => issueManualId && triggerIssue(issueManualId)}
890
- disabled={!issueManualId}
891
- className="text-[9px] px-3 py-1 bg-[var(--accent)] text-white rounded hover:opacity-90 disabled:opacity-50"
892
- >Fix Issue</button>
893
- </div>
810
+ {/* Bound pipeline list */}
811
+ {pipelineBindings.length === 0 ? (
812
+ <p className="text-[10px] text-[var(--text-secondary)]">No pipelines bound. Click + Add to attach a workflow.</p>
813
+ ) : (
814
+ pipelineBindings.map(b => (
815
+ <div key={b.workflowName} className="border border-[var(--border)] rounded p-3 space-y-2">
816
+ <div className="flex items-center gap-2">
817
+ <span className="text-[11px] font-semibold text-[var(--text-primary)]">{b.workflowName}</span>
818
+ <label className="flex items-center gap-1 text-[9px] text-[var(--text-secondary)] cursor-pointer ml-auto">
819
+ <input type="checkbox" checked={b.enabled} onChange={async (e) => {
820
+ await fetch('/api/project-pipelines', {
821
+ method: 'POST',
822
+ headers: { 'Content-Type': 'application/json' },
823
+ body: JSON.stringify({ action: 'update', projectPath, workflowName: b.workflowName, enabled: e.target.checked }),
824
+ });
825
+ fetchPipelineBindings();
826
+ }} className="accent-[var(--accent)]" />
827
+ Enabled
828
+ </label>
829
+ <button
830
+ onClick={() => triggerProjectPipeline(b.workflowName, triggerInput)}
831
+ className="text-[9px] px-2 py-0.5 border border-[var(--accent)] text-[var(--accent)] rounded hover:bg-[var(--accent)] hover:text-white"
832
+ >Run</button>
833
+ <button
834
+ onClick={async () => {
835
+ if (!confirm(`Remove "${b.workflowName}" from this project?`)) return;
836
+ await fetch('/api/project-pipelines', {
837
+ method: 'POST',
838
+ headers: { 'Content-Type': 'application/json' },
839
+ body: JSON.stringify({ action: 'remove', projectPath, workflowName: b.workflowName }),
840
+ });
841
+ fetchPipelineBindings();
842
+ }}
843
+ className="text-[9px] text-[var(--red)] hover:underline"
844
+ >Remove</button>
845
+ </div>
846
+ {/* Schedule config */}
847
+ <div className="flex items-center gap-2 text-[9px]">
848
+ <span className="text-[var(--text-secondary)]">Schedule:</span>
849
+ <select
850
+ value={b.config.interval || 0}
851
+ onChange={async (e) => {
852
+ const interval = Number(e.target.value);
853
+ const newConfig = { ...b.config, interval };
854
+ await fetch('/api/project-pipelines', {
855
+ method: 'POST',
856
+ headers: { 'Content-Type': 'application/json' },
857
+ body: JSON.stringify({ action: 'update', projectPath, workflowName: b.workflowName, config: newConfig }),
858
+ });
859
+ fetchPipelineBindings();
860
+ }}
861
+ className="bg-[var(--bg-secondary)] border border-[var(--border)] rounded px-1.5 py-0.5 text-[9px] text-[var(--text-primary)]"
862
+ >
863
+ <option value={0}>Manual only</option>
864
+ <option value={15}>Every 15 min</option>
865
+ <option value={30}>Every 30 min</option>
866
+ <option value={60}>Every 1 hour</option>
867
+ <option value={120}>Every 2 hours</option>
868
+ <option value={360}>Every 6 hours</option>
869
+ <option value={720}>Every 12 hours</option>
870
+ <option value={1440}>Every 24 hours</option>
871
+ </select>
872
+ {b.config.interval > 0 && b.nextRunAt && (
873
+ <span className="text-[8px] text-[var(--text-secondary)]">
874
+ Next: {new Date(b.nextRunAt).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
875
+ </span>
876
+ )}
877
+ {b.lastRunAt && (
878
+ <span className="text-[8px] text-[var(--text-secondary)] ml-auto">
879
+ Last: {new Date(b.lastRunAt).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
880
+ </span>
881
+ )}
882
+ </div>
883
+ </div>
884
+ ))
885
+ )}
894
886
  </div>
895
887
 
896
- {/* History */}
897
- {issueProcessed.length > 0 && (
888
+ {/* Execution history */}
889
+ {pipelineRuns.length > 0 && (
898
890
  <div className="border-t border-[var(--border)] pt-3">
899
- <div className="text-[9px] text-[var(--text-secondary)] uppercase mb-2">Processed Issues</div>
891
+ <div className="text-[9px] text-[var(--text-secondary)] uppercase mb-2">Execution History</div>
900
892
  <div className="border border-[var(--border)] rounded overflow-hidden">
901
- {issueProcessed.map(p => (
902
- <div key={p.issueNumber} className="border-b border-[var(--border)]/30 last:border-b-0">
903
- <div className="flex items-center gap-2 px-3 py-1.5 text-[10px]">
904
- <span className="text-[var(--text-primary)] font-mono">#{p.issueNumber}</span>
905
- <span className={`text-[8px] px-1 rounded ${
906
- p.status === 'done' ? 'bg-green-500/10 text-green-400' :
907
- p.status === 'failed' ? 'bg-red-500/10 text-red-400' :
908
- 'bg-yellow-500/10 text-yellow-400'
909
- }`}>{p.status}</span>
910
- {p.prNumber && <span className="text-[var(--accent)]">PR #{p.prNumber}</span>}
911
- {p.pipelineId && (
912
- <button
913
- onClick={() => {
914
- const event = new CustomEvent('forge:view-pipeline', { detail: { pipelineId: p.pipelineId } });
915
- window.dispatchEvent(event);
916
- }}
917
- className="text-[8px] text-[var(--text-secondary)] hover:text-[var(--accent)] font-mono"
918
- title="View pipeline"
919
- >{p.pipelineId.slice(0, 8)}</button>
920
- )}
921
- <span className="text-[var(--text-secondary)] text-[8px]">{p.createdAt}</span>
922
- <div className="ml-auto flex gap-1">
923
- {(p.status === 'failed' || p.status === 'done' || p.status === 'processing') && (
924
- <button
925
- onClick={() => setRetryModal({ issueNumber: p.issueNumber, context: '' })}
926
- className="text-[8px] text-[var(--accent)] hover:underline"
927
- >Retry</button>
928
- )}
929
- <button
930
- onClick={async () => {
931
- if (!confirm(`Delete record for issue #${p.issueNumber}?`)) return;
932
- await fetch('/api/issue-scanner', {
933
- method: 'POST',
934
- headers: { 'Content-Type': 'application/json' },
935
- body: JSON.stringify({ action: 'reset', projectPath, issueId: p.issueNumber }),
936
- });
937
- fetchIssueConfig();
938
- }}
939
- className="text-[8px] text-[var(--text-secondary)] hover:text-[var(--red)]"
940
- >Delete</button>
893
+ {pipelineRuns.map(run => (
894
+ <div key={run.id} className="flex items-start gap-2 px-3 py-2 border-b border-[var(--border)]/30 last:border-b-0 text-[10px]">
895
+ <span className={`shrink-0 ${
896
+ run.status === 'done' ? 'text-green-400' : run.status === 'failed' ? 'text-red-400' : 'text-yellow-400'
897
+ }`}>●</span>
898
+ <div className="flex-1 min-w-0">
899
+ <div className="flex items-center gap-2">
900
+ <span className="text-[var(--text-primary)] font-medium">{run.workflowName}</span>
901
+ <span className="text-[8px] text-[var(--text-secondary)] font-mono">{run.pipelineId.slice(0, 8)}</span>
902
+ <span className="text-[8px] text-[var(--text-secondary)] ml-auto">{new Date(run.createdAt).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}</span>
941
903
  </div>
904
+ {run.summary && (
905
+ <pre className="text-[9px] text-[var(--text-secondary)] mt-1 whitespace-pre-wrap break-words line-clamp-3">{run.summary}</pre>
906
+ )}
942
907
  </div>
908
+ <button
909
+ onClick={async () => {
910
+ if (!confirm('Delete this run?')) return;
911
+ await fetch('/api/project-pipelines', {
912
+ method: 'POST',
913
+ headers: { 'Content-Type': 'application/json' },
914
+ body: JSON.stringify({ action: 'delete-run', id: run.id }),
915
+ });
916
+ fetchPipelineBindings();
917
+ }}
918
+ className="text-[8px] text-[var(--text-secondary)] hover:text-[var(--red)] shrink-0"
919
+ >×</button>
943
920
  </div>
944
921
  ))}
945
922
  </div>
@@ -1026,50 +1003,6 @@ export default memo(function ProjectDetail({ projectPath, projectName, hasGit }:
1026
1003
  </div>
1027
1004
  )}
1028
1005
 
1029
- {/* Retry modal */}
1030
- {retryModal && (
1031
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={() => setRetryModal(null)}>
1032
- <div className="bg-[var(--bg-secondary)] border border-[var(--border)] rounded-lg shadow-xl w-[420px] max-h-[80vh] flex flex-col" onClick={e => e.stopPropagation()}>
1033
- <div className="px-4 py-3 border-b border-[var(--border)]">
1034
- <h3 className="text-sm font-semibold text-[var(--text-primary)]">Retry Issue #{retryModal.issueNumber}</h3>
1035
- <p className="text-[10px] text-[var(--text-secondary)] mt-0.5">Add context to help the AI fix the issue better this time.</p>
1036
- </div>
1037
- <div className="p-4">
1038
- <textarea
1039
- value={retryModal.context}
1040
- onChange={e => setRetryModal({ ...retryModal, context: e.target.value })}
1041
- placeholder="e.g. The previous fix caused a merge conflict. Rebase from main first, then fix only the validation logic in src/utils.ts..."
1042
- className="w-full h-32 px-3 py-2 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-[11px] text-[var(--text-primary)] placeholder:text-[var(--text-secondary)]/50 resize-none focus:outline-none focus:border-[var(--accent)]"
1043
- autoFocus
1044
- />
1045
- </div>
1046
- <div className="px-4 py-3 border-t border-[var(--border)] flex justify-end gap-2">
1047
- <button
1048
- onClick={() => setRetryModal(null)}
1049
- className="text-[11px] px-3 py-1.5 text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
1050
- >Cancel</button>
1051
- <button
1052
- onClick={async () => {
1053
- await fetch('/api/issue-scanner', {
1054
- method: 'POST',
1055
- headers: { 'Content-Type': 'application/json' },
1056
- body: JSON.stringify({
1057
- action: 'retry',
1058
- projectPath,
1059
- projectName,
1060
- issueId: retryModal.issueNumber,
1061
- context: retryModal.context,
1062
- }),
1063
- });
1064
- setRetryModal(null);
1065
- fetchIssueConfig();
1066
- }}
1067
- className="text-[11px] px-4 py-1.5 bg-[var(--accent)] text-white rounded hover:opacity-90"
1068
- >Retry</button>
1069
- </div>
1070
- </div>
1071
- </div>
1072
- )}
1073
1006
  </>
1074
1007
  );
1075
1008
  });
package/forge-logo.png ADDED
Binary file
@@ -50,15 +50,12 @@ nodes:
50
50
 
51
51
  ## Built-in Workflows
52
52
 
53
- ### issue-auto-fix
54
- Fetches a GitHub issue → fixes code on new branchcreates PR.
53
+ ### issue-fix-and-review
54
+ Complete issue resolution pipeline: fetch issue → fix code create PRreview code → notify.
55
55
 
56
- Input: `issue_id`, `project`, `base_branch` (optional)
56
+ Steps: setup fetch-issue → fix-code → push-and-pr → review → cleanup
57
57
 
58
- ### pr-review
59
- Fetches PR diff → AI code review → posts result.
60
-
61
- Input: `pr_number`, `project`
58
+ Input: `issue_id`, `project`, `base_branch` (optional), `extra_context` (optional)
62
59
 
63
60
  ## CLI
64
61
 
@@ -67,6 +64,24 @@ forge flows # list available workflows
67
64
  forge run my-workflow # execute a workflow
68
65
  ```
69
66
 
67
+ ## Import a Workflow
68
+
69
+ 1. In Pipelines tab, click **Import**
70
+ 2. Paste YAML workflow content
71
+ 3. Click **Save Workflow**
72
+
73
+ Or save YAML directly to `~/.forge/data/flows/<name>.yaml`.
74
+
75
+ To create a workflow via Help AI: ask "Create a pipeline that does X" — the AI will generate the YAML for you to import.
76
+
77
+ ## Creating Workflows via API
78
+
79
+ ```bash
80
+ curl -X POST http://localhost:3000/api/pipelines \
81
+ -H 'Content-Type: application/json' \
82
+ -d '{"action": "save-workflow", "yaml": "name: my-flow\nnodes:\n step1:\n project: my-project\n prompt: do something"}'
83
+ ```
84
+
70
85
  ## Storage
71
86
 
72
87
  - Workflow YAML: `~/.forge/data/flows/`
@@ -28,7 +28,7 @@ Scan → Fetch Issue → Fix Code (new branch) → Push → Create PR → Auto R
28
28
  1. **Scan**: `gh issue list` finds open issues matching labels
29
29
  2. **Fix**: Claude Code analyzes issue and fixes code on `fix/<id>-<description>` branch
30
30
  3. **PR**: Pushes branch and creates Pull Request
31
- 4. **Review**: Automatically triggers `pr-review` pipeline
31
+ 4. **Review**: AI reviews the code changes in the same pipeline
32
32
  5. **Notify**: Results sent via Telegram (if configured)
33
33
 
34
34
  ## Manual Trigger
package/lib/init.ts CHANGED
@@ -95,7 +95,13 @@ export function ensureInitialized() {
95
95
  // Session watcher is safe (file-based, idempotent)
96
96
  startWatcherLoop();
97
97
 
98
- // Issue scannerauto-scan GitHub issues for configured projects
98
+ // Pipeline schedulerperiodic execution for project-bound workflows
99
+ try {
100
+ const { startScheduler } = require('./pipeline-scheduler');
101
+ startScheduler();
102
+ } catch {}
103
+
104
+ // Legacy issue scanner (still used if issue_autofix_config has entries)
99
105
  try {
100
106
  const { startScanner } = require('./issue-scanner');
101
107
  startScanner();
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Issue Scanner — periodically scans GitHub issues for configured projects
3
- * and triggers issue-auto-fix pipeline for new issues.
3
+ * and triggers issue-fix-and-review pipeline for new issues.
4
4
  *
5
5
  * Per-project config stored in DB:
6
6
  * - enabled: boolean
@@ -196,7 +196,7 @@ export function scanAndTrigger(config: IssueAutofixConfig): { triggered: number;
196
196
  if (isProcessed(config.projectPath, issue.number)) continue;
197
197
 
198
198
  try {
199
- const pipeline = startPipeline('issue-auto-fix', {
199
+ const pipeline = startPipeline('issue-fix-and-review', {
200
200
  issue_id: String(issue.number),
201
201
  project: config.projectName,
202
202
  base_branch: config.baseBranch || 'auto-detect',