claude-controller 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +2 -2
  2. package/bin/autoloop.sh +382 -0
  3. package/bin/ctl +1189 -0
  4. package/bin/native-app.py +6 -3
  5. package/bin/watchdog.sh +357 -0
  6. package/cognitive/__init__.py +14 -0
  7. package/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  8. package/cognitive/__pycache__/dispatcher.cpython-314.pyc +0 -0
  9. package/cognitive/__pycache__/evaluator.cpython-314.pyc +0 -0
  10. package/cognitive/__pycache__/goal_engine.cpython-314.pyc +0 -0
  11. package/cognitive/__pycache__/learning.cpython-314.pyc +0 -0
  12. package/cognitive/__pycache__/orchestrator.cpython-314.pyc +0 -0
  13. package/cognitive/__pycache__/planner.cpython-314.pyc +0 -0
  14. package/cognitive/dispatcher.py +192 -0
  15. package/cognitive/evaluator.py +289 -0
  16. package/cognitive/goal_engine.py +232 -0
  17. package/cognitive/learning.py +189 -0
  18. package/cognitive/orchestrator.py +303 -0
  19. package/cognitive/planner.py +207 -0
  20. package/cognitive/prompts/analyst.md +31 -0
  21. package/cognitive/prompts/coder.md +22 -0
  22. package/cognitive/prompts/reviewer.md +33 -0
  23. package/cognitive/prompts/tester.md +21 -0
  24. package/cognitive/prompts/writer.md +25 -0
  25. package/config.sh +6 -1
  26. package/dag/__init__.py +5 -0
  27. package/dag/__pycache__/__init__.cpython-314.pyc +0 -0
  28. package/dag/__pycache__/graph.cpython-314.pyc +0 -0
  29. package/dag/graph.py +222 -0
  30. package/lib/jobs.sh +12 -1
  31. package/package.json +11 -5
  32. package/postinstall.sh +1 -1
  33. package/service/controller.sh +43 -11
  34. package/web/audit.py +122 -0
  35. package/web/checkpoint.py +80 -0
  36. package/web/config.py +2 -5
  37. package/web/handler.py +634 -473
  38. package/web/handler_fs.py +153 -0
  39. package/web/handler_goals.py +203 -0
  40. package/web/handler_jobs.py +372 -0
  41. package/web/handler_memory.py +203 -0
  42. package/web/handler_sessions.py +132 -0
  43. package/web/jobs.py +585 -13
  44. package/web/personas.py +419 -0
  45. package/web/pipeline.py +981 -0
  46. package/web/presets.py +506 -0
  47. package/web/projects.py +246 -0
  48. package/web/static/api.js +141 -0
  49. package/web/static/app.js +25 -1937
  50. package/web/static/attachments.js +144 -0
  51. package/web/static/base.css +497 -0
  52. package/web/static/context.js +204 -0
  53. package/web/static/dirs.js +246 -0
  54. package/web/static/form.css +763 -0
  55. package/web/static/goals.css +363 -0
  56. package/web/static/goals.js +300 -0
  57. package/web/static/i18n.js +625 -0
  58. package/web/static/index.html +215 -13
  59. package/web/static/{styles.css → jobs.css} +746 -1141
  60. package/web/static/jobs.js +1270 -0
  61. package/web/static/memoryview.js +117 -0
  62. package/web/static/personas.js +228 -0
  63. package/web/static/pipeline.css +338 -0
  64. package/web/static/pipelines.js +487 -0
  65. package/web/static/presets.js +244 -0
  66. package/web/static/send.js +135 -0
  67. package/web/static/settings-style.css +291 -0
  68. package/web/static/settings.js +81 -0
  69. package/web/static/stream.js +534 -0
  70. package/web/static/utils.js +131 -0
  71. package/web/webhook.py +210 -0
@@ -0,0 +1,363 @@
1
+ /* ═══════════════════════════════════════════════
2
+ Goals — 목표 관리 + DAG 시각화 스타일
3
+ ═══════════════════════════════════════════════ */
4
+
5
+ /* ── Goal Filter Bar ── */
6
+ .goal-filter-bar {
7
+ display: flex;
8
+ align-items: center;
9
+ gap: 8px;
10
+ padding: 8px 16px;
11
+ border-bottom: 1px solid var(--border);
12
+ }
13
+ .goal-filter-btns {
14
+ display: flex;
15
+ gap: 2px;
16
+ }
17
+ .goal-filter-btn {
18
+ padding: 4px 12px;
19
+ font-size: 0.72rem;
20
+ border: none;
21
+ background: transparent;
22
+ color: var(--text-muted);
23
+ border-radius: 4px;
24
+ cursor: pointer;
25
+ transition: all var(--transition);
26
+ }
27
+ .goal-filter-btn:hover { background: var(--surface-hover); color: var(--text-secondary); }
28
+ .goal-filter-btn.active { background: var(--accent-glow); color: var(--accent); font-weight: 500; }
29
+
30
+ /* ── Goal List ── */
31
+ .goal-list {
32
+ display: flex;
33
+ flex-direction: column;
34
+ }
35
+
36
+ /* ── Goal Card ── */
37
+ .goal-card {
38
+ padding: 14px 16px;
39
+ border-bottom: 1px solid var(--border);
40
+ cursor: pointer;
41
+ transition: background var(--transition);
42
+ }
43
+ .goal-card:hover { background: var(--surface-hover); }
44
+ .goal-card:last-child { border-bottom: none; }
45
+ .goal-card.expanded { background: var(--surface-hover); }
46
+
47
+ .goal-card-header {
48
+ display: flex;
49
+ align-items: flex-start;
50
+ gap: 10px;
51
+ }
52
+ .goal-card-status {
53
+ flex-shrink: 0;
54
+ width: 8px;
55
+ height: 8px;
56
+ border-radius: 50%;
57
+ margin-top: 5px;
58
+ }
59
+ .goal-card-status.pending { background: var(--text-muted); }
60
+ .goal-card-status.planning { background: var(--yellow); animation: pulse-glow 2s infinite; }
61
+ .goal-card-status.ready { background: var(--blue); }
62
+ .goal-card-status.running { background: var(--blue); animation: pulse-glow 1.5s infinite; }
63
+ .goal-card-status.gate_waiting { background: var(--yellow); }
64
+ .goal-card-status.evaluating { background: var(--yellow); animation: pulse-glow 2s infinite; }
65
+ .goal-card-status.completed { background: var(--green); }
66
+ .goal-card-status.failed { background: var(--red); }
67
+ .goal-card-status.cancelled { background: var(--text-muted); opacity: 0.5; }
68
+
69
+ @keyframes pulse-glow {
70
+ 0%, 100% { opacity: 1; }
71
+ 50% { opacity: 0.4; }
72
+ }
73
+
74
+ .goal-card-body { flex: 1; min-width: 0; }
75
+ .goal-card-objective {
76
+ font-size: 0.82rem;
77
+ font-weight: 500;
78
+ color: var(--text);
79
+ line-height: 1.4;
80
+ margin-bottom: 4px;
81
+ }
82
+ .goal-card-meta {
83
+ display: flex;
84
+ align-items: center;
85
+ gap: 12px;
86
+ font-size: 0.68rem;
87
+ color: var(--text-muted);
88
+ }
89
+ .goal-card-meta .goal-mode-badge {
90
+ padding: 1px 6px;
91
+ border-radius: 3px;
92
+ background: var(--accent-glow);
93
+ color: var(--accent);
94
+ font-weight: 500;
95
+ font-size: 0.65rem;
96
+ text-transform: uppercase;
97
+ }
98
+ .goal-card-meta .goal-status-label {
99
+ font-weight: 500;
100
+ }
101
+ .goal-card-actions {
102
+ display: flex;
103
+ gap: 4px;
104
+ flex-shrink: 0;
105
+ }
106
+ .goal-card-actions .btn {
107
+ padding: 3px 8px;
108
+ font-size: 0.68rem;
109
+ }
110
+
111
+ /* ── Goal Progress Bar ── */
112
+ .goal-progress-bar {
113
+ height: 3px;
114
+ background: var(--border);
115
+ border-radius: 2px;
116
+ margin-top: 8px;
117
+ overflow: hidden;
118
+ }
119
+ .goal-progress-fill {
120
+ height: 100%;
121
+ background: var(--accent);
122
+ border-radius: 2px;
123
+ transition: width 0.5s ease;
124
+ }
125
+ .goal-progress-fill.done { background: var(--green); }
126
+ .goal-progress-fill.failed { background: var(--red); }
127
+
128
+ /* ── Goal Detail (expanded) ── */
129
+ .goal-detail {
130
+ padding: 12px 16px 16px 34px;
131
+ display: none;
132
+ }
133
+ .goal-card.expanded .goal-detail { display: block; }
134
+
135
+ /* ── DAG Tree View ── */
136
+ .dag-tree {
137
+ margin-top: 8px;
138
+ }
139
+ .dag-tree ul {
140
+ list-style: none;
141
+ padding-left: 20px;
142
+ position: relative;
143
+ }
144
+ .dag-tree > ul { padding-left: 0; }
145
+
146
+ .dag-tree li {
147
+ position: relative;
148
+ padding: 4px 0 4px 18px;
149
+ }
150
+ .dag-tree li::before {
151
+ content: '';
152
+ position: absolute;
153
+ left: 0;
154
+ top: 0;
155
+ bottom: 0;
156
+ width: 1px;
157
+ background: var(--border-light);
158
+ }
159
+ .dag-tree li::after {
160
+ content: '';
161
+ position: absolute;
162
+ left: 0;
163
+ top: 14px;
164
+ width: 12px;
165
+ height: 1px;
166
+ background: var(--border-light);
167
+ }
168
+ .dag-tree li:last-child::before { height: 14px; }
169
+
170
+ .dag-node {
171
+ display: inline-flex;
172
+ align-items: center;
173
+ gap: 6px;
174
+ padding: 4px 10px;
175
+ border-radius: 4px;
176
+ font-size: 0.72rem;
177
+ background: var(--surface);
178
+ border: 1px solid var(--border);
179
+ transition: all var(--transition);
180
+ }
181
+ .dag-node:hover { border-color: var(--accent); }
182
+
183
+ .dag-node-dot {
184
+ width: 6px;
185
+ height: 6px;
186
+ border-radius: 50%;
187
+ flex-shrink: 0;
188
+ }
189
+ .dag-node-dot.pending { background: var(--text-muted); }
190
+ .dag-node-dot.running { background: var(--blue); animation: pulse-glow 1.5s infinite; }
191
+ .dag-node-dot.completed { background: var(--green); }
192
+ .dag-node-dot.failed { background: var(--red); }
193
+
194
+ .dag-node-label { color: var(--text); }
195
+ .dag-node-type {
196
+ font-size: 0.62rem;
197
+ color: var(--text-muted);
198
+ font-family: var(--font-mono, monospace);
199
+ }
200
+
201
+ /* ── Goal Create Form ── */
202
+ .goal-create-form {
203
+ padding: 12px 16px;
204
+ border-bottom: 1px solid var(--border);
205
+ display: none;
206
+ }
207
+ .goal-create-form.visible { display: block; }
208
+ .goal-create-row {
209
+ display: flex;
210
+ gap: 8px;
211
+ align-items: flex-end;
212
+ margin-bottom: 8px;
213
+ }
214
+ .goal-create-row .form-field { flex: 1; }
215
+ .goal-create-row .form-field label {
216
+ display: block;
217
+ font-size: 0.68rem;
218
+ color: var(--text-secondary);
219
+ margin-bottom: 3px;
220
+ }
221
+ .goal-create-row .form-field input,
222
+ .goal-create-row .form-field select,
223
+ .goal-create-row .form-field textarea {
224
+ width: 100%;
225
+ padding: 6px 10px;
226
+ font-size: 0.78rem;
227
+ background: var(--bg);
228
+ border: 1px solid var(--border);
229
+ border-radius: var(--radius);
230
+ color: var(--text);
231
+ font-family: var(--font);
232
+ }
233
+ .goal-create-row .form-field textarea {
234
+ resize: vertical;
235
+ min-height: 36px;
236
+ }
237
+ .goal-create-actions {
238
+ display: flex;
239
+ justify-content: flex-end;
240
+ gap: 6px;
241
+ }
242
+
243
+ /* ── Memory Section ── */
244
+ .memory-filter-bar {
245
+ display: flex;
246
+ align-items: center;
247
+ gap: 8px;
248
+ padding: 8px 16px;
249
+ border-bottom: 1px solid var(--border);
250
+ }
251
+ .memory-search-wrap {
252
+ flex: 1;
253
+ display: flex;
254
+ align-items: center;
255
+ gap: 6px;
256
+ }
257
+ .memory-search-wrap input {
258
+ flex: 1;
259
+ padding: 5px 10px;
260
+ font-size: 0.75rem;
261
+ background: var(--bg);
262
+ border: 1px solid var(--border);
263
+ border-radius: var(--radius);
264
+ color: var(--text);
265
+ }
266
+ .memory-type-select {
267
+ padding: 5px 8px;
268
+ font-size: 0.72rem;
269
+ background: var(--bg);
270
+ border: 1px solid var(--border);
271
+ border-radius: var(--radius);
272
+ color: var(--text);
273
+ }
274
+
275
+ /* ── Memory Card ── */
276
+ .memory-list {
277
+ display: flex;
278
+ flex-direction: column;
279
+ }
280
+ .memory-card {
281
+ padding: 12px 16px;
282
+ border-bottom: 1px solid var(--border);
283
+ transition: background var(--transition);
284
+ }
285
+ .memory-card:hover { background: var(--surface-hover); }
286
+ .memory-card:last-child { border-bottom: none; }
287
+
288
+ .memory-card-header {
289
+ display: flex;
290
+ align-items: center;
291
+ gap: 8px;
292
+ margin-bottom: 4px;
293
+ }
294
+ .memory-type-badge {
295
+ padding: 1px 6px;
296
+ border-radius: 3px;
297
+ font-size: 0.62rem;
298
+ font-weight: 600;
299
+ text-transform: uppercase;
300
+ }
301
+ .memory-type-badge.decision { background: var(--blue-dim); color: var(--blue); }
302
+ .memory-type-badge.pattern { background: var(--green-dim); color: var(--green); }
303
+ .memory-type-badge.failure { background: var(--red-dim); color: var(--red); }
304
+ .memory-type-badge.context { background: var(--yellow-dim); color: var(--yellow); }
305
+
306
+ .memory-card-title {
307
+ font-size: 0.78rem;
308
+ font-weight: 500;
309
+ color: var(--text);
310
+ flex: 1;
311
+ }
312
+ .memory-card-actions {
313
+ display: flex;
314
+ gap: 4px;
315
+ }
316
+ .memory-card-actions button {
317
+ padding: 2px 6px;
318
+ background: transparent;
319
+ border: none;
320
+ color: var(--text-muted);
321
+ cursor: pointer;
322
+ border-radius: 3px;
323
+ transition: all var(--transition);
324
+ }
325
+ .memory-card-actions button:hover { background: var(--surface-active); color: var(--text); }
326
+
327
+ .memory-card-content {
328
+ font-size: 0.72rem;
329
+ color: var(--text-secondary);
330
+ line-height: 1.5;
331
+ margin-bottom: 4px;
332
+ white-space: pre-wrap;
333
+ max-height: 60px;
334
+ overflow: hidden;
335
+ }
336
+ .memory-card-tags {
337
+ display: flex;
338
+ gap: 4px;
339
+ flex-wrap: wrap;
340
+ }
341
+ .memory-tag {
342
+ padding: 1px 6px;
343
+ font-size: 0.62rem;
344
+ background: var(--surface-active);
345
+ color: var(--text-muted);
346
+ border-radius: 3px;
347
+ }
348
+
349
+ /* ── Memory Create Form ── */
350
+ .memory-create-form {
351
+ padding: 12px 16px;
352
+ border-bottom: 1px solid var(--border);
353
+ display: none;
354
+ }
355
+ .memory-create-form.visible { display: block; }
356
+
357
+ /* ── Empty State ── */
358
+ .goal-empty, .memory-empty {
359
+ padding: 32px 20px;
360
+ text-align: center;
361
+ color: var(--text-muted);
362
+ font-size: 0.8rem;
363
+ }
@@ -0,0 +1,300 @@
1
+ /* ═══════════════════════════════════════════════
2
+ Goals — 목표 관리 UI (CRUD + DAG 트리 뷰 + Gate 승인)
3
+ ═══════════════════════════════════════════════ */
4
+
5
+ let _goalFilter = 'all';
6
+ let _goalsCache = [];
7
+ let _goalPollTimer = null;
8
+
9
+ /* ── Status helpers ── */
10
+ const _GOAL_STATUS_ICON = {
11
+ pending: 'pending', planning: 'planning', ready: 'ready',
12
+ running: 'running', gate_waiting: 'gate_waiting', evaluating: 'evaluating',
13
+ completed: 'completed', failed: 'failed', cancelled: 'cancelled',
14
+ };
15
+
16
+ function _isActiveGoal(g) {
17
+ return !['completed', 'failed', 'cancelled'].includes(g.status);
18
+ }
19
+
20
+ /* ── Fetch & Render ── */
21
+ async function loadGoals() {
22
+ try {
23
+ const data = await fetchGoals();
24
+ _goalsCache = Array.isArray(data) ? data : (data.goals || []);
25
+ renderGoals();
26
+ _updateGoalCount();
27
+ _startGoalPoll();
28
+ } catch (e) {
29
+ const list = document.getElementById('goalList');
30
+ if (list) list.innerHTML = `<div class="goal-empty">${t('msg_goal_failed')}: ${e.message}</div>`;
31
+ }
32
+ }
33
+
34
+ function _updateGoalCount() {
35
+ const el = document.getElementById('goalCount');
36
+ if (!el) return;
37
+ const active = _goalsCache.filter(_isActiveGoal).length;
38
+ el.textContent = active > 0 ? `${active}` : '';
39
+ }
40
+
41
+ function renderGoals() {
42
+ const list = document.getElementById('goalList');
43
+ if (!list) return;
44
+
45
+ const filtered = _goalFilter === 'all' ? _goalsCache
46
+ : _goalFilter === 'active' ? _goalsCache.filter(_isActiveGoal)
47
+ : _goalFilter === 'done' ? _goalsCache.filter(g => g.status === 'completed')
48
+ : _goalsCache.filter(g => g.status === 'cancelled');
49
+
50
+ if (filtered.length === 0) {
51
+ list.innerHTML = `<div class="goal-empty">${t('goal_no_goals')}</div>`;
52
+ return;
53
+ }
54
+
55
+ list.innerHTML = filtered.map(g => _renderGoalCard(g)).join('');
56
+ }
57
+
58
+ function _renderGoalCard(g) {
59
+ const statusLabel = t(`goal_status_${g.status}`) || g.status;
60
+ const modeLabel = t(`goal_mode_${g.mode}`) || g.mode;
61
+ const progress = _calcProgress(g);
62
+ const progressClass = g.status === 'completed' ? 'done' : g.status === 'failed' ? 'failed' : '';
63
+ const created = g.created_at ? _timeAgo(g.created_at) : '';
64
+ const budget = g.budget_usd != null ? `$${Number(g.budget_usd).toFixed(2)}` : '';
65
+
66
+ let actions = '';
67
+ if (g.status === 'gate_waiting') {
68
+ actions += `<button class="btn btn-sm btn-primary" onclick="event.stopPropagation();onApproveGoal('${g.id}')">${t('goal_approve')}</button>`;
69
+ }
70
+ if (_isActiveGoal(g)) {
71
+ actions += `<button class="btn btn-sm" onclick="event.stopPropagation();onCancelGoal('${g.id}')" title="${t('goal_cancel')}"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>`;
72
+ }
73
+
74
+ return `<div class="goal-card" id="goal-${g.id}" onclick="toggleGoalDetail('${g.id}')">
75
+ <div class="goal-card-header">
76
+ <span class="goal-card-status ${g.status}"></span>
77
+ <div class="goal-card-body">
78
+ <div class="goal-card-objective">${_esc(g.objective)}</div>
79
+ <div class="goal-card-meta">
80
+ <span class="goal-mode-badge">${modeLabel}</span>
81
+ <span class="goal-status-label">${statusLabel}</span>
82
+ ${budget ? `<span>${budget}</span>` : ''}
83
+ ${created ? `<span>${created}</span>` : ''}
84
+ </div>
85
+ ${progress >= 0 ? `<div class="goal-progress-bar"><div class="goal-progress-fill ${progressClass}" style="width:${progress}%"></div></div>` : ''}
86
+ </div>
87
+ <div class="goal-card-actions">${actions}</div>
88
+ </div>
89
+ <div class="goal-detail" id="goal-detail-${g.id}"></div>
90
+ </div>`;
91
+ }
92
+
93
+ function _calcProgress(g) {
94
+ const dag = g.dag || g.tasks;
95
+ if (!dag) return -1;
96
+ const tasks = Array.isArray(dag) ? dag : (dag.nodes || []);
97
+ if (tasks.length === 0) return 0;
98
+ const done = tasks.filter(t => t.status === 'completed').length;
99
+ return Math.round((done / tasks.length) * 100);
100
+ }
101
+
102
+ /* ── Detail toggle ── */
103
+ async function toggleGoalDetail(id) {
104
+ const card = document.getElementById(`goal-${id}`);
105
+ if (!card) return;
106
+ if (card.classList.contains('expanded')) {
107
+ card.classList.remove('expanded');
108
+ return;
109
+ }
110
+ // collapse others
111
+ document.querySelectorAll('.goal-card.expanded').forEach(c => c.classList.remove('expanded'));
112
+ card.classList.add('expanded');
113
+
114
+ const detail = document.getElementById(`goal-detail-${id}`);
115
+ if (!detail) return;
116
+ detail.innerHTML = `<div style="color:var(--text-muted);font-size:0.72rem;">...</div>`;
117
+
118
+ try {
119
+ const goal = await getGoal(id);
120
+ detail.innerHTML = _renderGoalDetail(goal);
121
+ } catch (e) {
122
+ detail.innerHTML = `<div style="color:var(--red);font-size:0.72rem;">${e.message}</div>`;
123
+ }
124
+ }
125
+
126
+ function _renderGoalDetail(g) {
127
+ let html = '';
128
+
129
+ // Success criteria
130
+ if (g.success_criteria && g.success_criteria.length) {
131
+ html += `<div style="margin-bottom:10px;"><div style="font-size:0.72rem;font-weight:500;color:var(--text-secondary);margin-bottom:4px;">Success Criteria</div>`;
132
+ html += g.success_criteria.map(c => `<div style="font-size:0.7rem;color:var(--text-muted);padding-left:8px;">• ${_esc(c)}</div>`).join('');
133
+ html += `</div>`;
134
+ }
135
+
136
+ // DAG tree
137
+ const tasks = g.dag?.nodes || g.tasks || g.next_tasks || [];
138
+ if (tasks.length > 0) {
139
+ html += `<div style="font-size:0.72rem;font-weight:500;color:var(--text-secondary);margin-bottom:4px;">${t('goal_dag')}</div>`;
140
+ html += `<div class="dag-tree">${_renderDagTree(tasks)}</div>`;
141
+ }
142
+
143
+ // Context
144
+ if (g.context && Object.keys(g.context).length) {
145
+ html += `<div style="margin-top:10px;font-size:0.68rem;color:var(--text-muted);font-family:var(--font-mono,monospace);white-space:pre-wrap;max-height:100px;overflow:auto;">${_esc(JSON.stringify(g.context, null, 2))}</div>`;
146
+ }
147
+
148
+ if (!html) {
149
+ html = `<div style="font-size:0.72rem;color:var(--text-muted);">${t('goal_status_' + g.status)}</div>`;
150
+ }
151
+
152
+ return html;
153
+ }
154
+
155
+ /* ── DAG Tree Rendering (pure CSS tree) ── */
156
+ function _renderDagTree(nodes) {
157
+ if (!nodes || nodes.length === 0) return '';
158
+ // Build adjacency: find root nodes (no dependencies or deps all outside)
159
+ const nodeMap = {};
160
+ nodes.forEach(n => { nodeMap[n.id || n.task_id] = n; });
161
+ const childSet = new Set();
162
+ nodes.forEach(n => {
163
+ (n.depends_on || n.dependencies || []).forEach(dep => childSet.add(n.id || n.task_id));
164
+ });
165
+ const roots = nodes.filter(n => {
166
+ const deps = n.depends_on || n.dependencies || [];
167
+ return deps.length === 0;
168
+ });
169
+
170
+ if (roots.length === 0) {
171
+ // no dependency info — render flat
172
+ return `<ul>${nodes.map(n => _dagNodeLi(n)).join('')}</ul>`;
173
+ }
174
+
175
+ // Build children map
176
+ const children = {};
177
+ nodes.forEach(n => {
178
+ (n.depends_on || n.dependencies || []).forEach(dep => {
179
+ if (!children[dep]) children[dep] = [];
180
+ children[dep].push(n);
181
+ });
182
+ });
183
+
184
+ function buildUl(nodeList) {
185
+ return `<ul>${nodeList.map(n => {
186
+ const nid = n.id || n.task_id;
187
+ const kids = children[nid] || [];
188
+ return `<li>${_dagNodeHtml(n)}${kids.length ? buildUl(kids) : ''}</li>`;
189
+ }).join('')}</ul>`;
190
+ }
191
+
192
+ return buildUl(roots);
193
+ }
194
+
195
+ function _dagNodeLi(n) {
196
+ return `<li>${_dagNodeHtml(n)}</li>`;
197
+ }
198
+
199
+ function _dagNodeHtml(n) {
200
+ const status = n.status || 'pending';
201
+ const label = n.description || n.title || n.objective || n.id || n.task_id || '?';
202
+ const type = n.worker_type || n.type || '';
203
+ return `<span class="dag-node"><span class="dag-node-dot ${status}"></span><span class="dag-node-label">${_esc(label)}</span>${type ? `<span class="dag-node-type">${_esc(type)}</span>` : ''}</span>`;
204
+ }
205
+
206
+ /* ── Goal Create ── */
207
+ function toggleGoalForm() {
208
+ const form = document.getElementById('goalCreateForm');
209
+ if (!form) return;
210
+ form.classList.toggle('visible');
211
+ if (form.classList.contains('visible')) {
212
+ const input = document.getElementById('goalObjectiveInput');
213
+ if (input) input.focus();
214
+ }
215
+ }
216
+
217
+ async function submitGoalForm() {
218
+ const objective = (document.getElementById('goalObjectiveInput')?.value || '').trim();
219
+ if (!objective) return showToast(t('msg_prompt_required'), 'error');
220
+
221
+ const mode = document.getElementById('goalModeSelect')?.value || 'gate';
222
+ const budget = parseFloat(document.getElementById('goalBudgetInput')?.value) || 5.0;
223
+
224
+ try {
225
+ await createGoal(objective, mode, { budget_usd: budget });
226
+ showToast(t('msg_goal_created'));
227
+ document.getElementById('goalObjectiveInput').value = '';
228
+ document.getElementById('goalCreateForm')?.classList.remove('visible');
229
+ loadGoals();
230
+ } catch (e) {
231
+ showToast(`${t('msg_goal_failed')}: ${e.message}`, 'error');
232
+ }
233
+ }
234
+
235
+ /* ── Goal Actions ── */
236
+ async function onApproveGoal(id) {
237
+ try {
238
+ await approveGoal(id);
239
+ showToast(t('msg_goal_approved'));
240
+ loadGoals();
241
+ } catch (e) {
242
+ showToast(`${t('msg_goal_failed')}: ${e.message}`, 'error');
243
+ }
244
+ }
245
+
246
+ async function onCancelGoal(id) {
247
+ if (!confirm(t('goal_confirm_cancel'))) return;
248
+ try {
249
+ await cancelGoal(id);
250
+ showToast(t('msg_goal_cancelled'));
251
+ loadGoals();
252
+ } catch (e) {
253
+ showToast(`${t('msg_goal_failed')}: ${e.message}`, 'error');
254
+ }
255
+ }
256
+
257
+ /* ── Filter ── */
258
+ function setGoalFilter(filter) {
259
+ _goalFilter = filter;
260
+ document.querySelectorAll('.goal-filter-btn').forEach(btn => {
261
+ btn.classList.toggle('active', btn.dataset.filter === filter);
262
+ });
263
+ renderGoals();
264
+ }
265
+
266
+ /* ── Polling (active goals) ── */
267
+ function _startGoalPoll() {
268
+ if (_goalPollTimer) return;
269
+ const hasActive = _goalsCache.some(_isActiveGoal);
270
+ if (!hasActive) return;
271
+ _goalPollTimer = setInterval(async () => {
272
+ try {
273
+ const data = await fetchGoals();
274
+ _goalsCache = Array.isArray(data) ? data : (data.goals || []);
275
+ renderGoals();
276
+ _updateGoalCount();
277
+ if (!_goalsCache.some(_isActiveGoal)) _stopGoalPoll();
278
+ } catch { /* silent */ }
279
+ }, 5000);
280
+ }
281
+
282
+ function _stopGoalPoll() {
283
+ if (_goalPollTimer) { clearInterval(_goalPollTimer); _goalPollTimer = null; }
284
+ }
285
+
286
+ /* ── Util ── */
287
+ function _esc(s) {
288
+ if (!s) return '';
289
+ const d = document.createElement('div');
290
+ d.textContent = String(s);
291
+ return d.innerHTML;
292
+ }
293
+
294
+ function _timeAgo(ts) {
295
+ const sec = Math.floor(Date.now() / 1000 - ts);
296
+ if (sec < 60) return `${sec}s`;
297
+ if (sec < 3600) return `${Math.floor(sec / 60)}m`;
298
+ if (sec < 86400) return `${Math.floor(sec / 3600)}h`;
299
+ return `${Math.floor(sec / 86400)}d`;
300
+ }