trackops 1.0.1 → 1.1.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.
- package/README.md +326 -270
- package/bin/trackops.js +102 -70
- package/lib/config.js +260 -35
- package/lib/control.js +517 -475
- package/lib/env.js +227 -0
- package/lib/i18n.js +61 -53
- package/lib/init.js +135 -46
- package/lib/locale.js +63 -0
- package/lib/opera-bootstrap.js +523 -0
- package/lib/opera.js +319 -170
- package/lib/registry.js +27 -13
- package/lib/release.js +56 -0
- package/lib/resources.js +42 -0
- package/lib/server.js +907 -554
- package/lib/skills.js +148 -124
- package/lib/workspace.js +260 -0
- package/locales/en.json +331 -139
- package/locales/es.json +331 -139
- package/package.json +7 -9
- package/scripts/skills-marketplace-smoke.js +124 -0
- package/scripts/smoke-tests.js +445 -0
- package/scripts/sync-skill-version.js +21 -0
- package/scripts/validate-skill.js +88 -0
- package/skills/trackops/SKILL.md +64 -0
- package/skills/trackops/agents/openai.yaml +3 -0
- package/skills/trackops/references/activation.md +39 -0
- package/skills/trackops/references/troubleshooting.md +34 -0
- package/skills/trackops/references/workflow.md +20 -0
- package/skills/trackops/scripts/bootstrap-trackops.js +201 -0
- package/skills/trackops/skill.json +29 -0
- package/templates/opera/en/agent.md +26 -0
- package/templates/opera/en/genesis.md +79 -0
- package/templates/opera/en/references/autonomy-and-recovery.md +23 -0
- package/templates/opera/en/references/opera-cycle.md +62 -0
- package/templates/opera/en/registry.md +28 -0
- package/templates/opera/en/router.md +39 -0
- package/templates/opera/genesis.md +79 -94
- package/templates/skills/changelog-updater/locales/en/SKILL.md +11 -0
- package/templates/skills/commiter/locales/en/SKILL.md +11 -0
- package/templates/skills/project-starter-skill/locales/en/SKILL.md +24 -0
- package/ui/css/panels.css +956 -953
- package/ui/index.html +1 -1
- package/ui/js/api.js +211 -194
- package/ui/js/app.js +200 -199
- package/ui/js/i18n.js +14 -0
- package/ui/js/onboarding.js +439 -437
- package/ui/js/state.js +130 -129
- package/ui/js/utils.js +175 -172
- package/ui/js/views/board.js +255 -254
- package/ui/js/views/execution.js +256 -256
- package/ui/js/views/insights.js +340 -339
- package/ui/js/views/overview.js +365 -364
- package/ui/js/views/settings.js +340 -202
- package/ui/js/views/sidebar.js +131 -132
- package/ui/js/views/skills.js +163 -162
- package/ui/js/views/tasks.js +406 -405
- package/ui/js/views/topbar.js +239 -183
package/ui/js/views/board.js
CHANGED
|
@@ -1,254 +1,255 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* board.js — Tablero Kanban con drag & drop
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { icon } from '../icons.js';
|
|
6
|
-
import * as state from '../state.js';
|
|
7
|
-
import * as api from '../api.js';
|
|
8
|
-
import * as router from '../router.js';
|
|
9
|
-
import { flash } from './flash.js';
|
|
10
|
-
import { esc, debounce } from '../utils.js';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
{ id: '
|
|
15
|
-
{ id: '
|
|
16
|
-
{ id: '
|
|
17
|
-
{ id: '
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
t.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
${tasks.
|
|
95
|
-
|
|
96
|
-
: ''}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
data-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
aria-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
<
|
|
123
|
-
<
|
|
124
|
-
<
|
|
125
|
-
|
|
126
|
-
<span class="badge badge
|
|
127
|
-
|
|
128
|
-
${task.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
c.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
card.
|
|
192
|
-
|
|
193
|
-
e.dataTransfer.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
card
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
col
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* board.js — Tablero Kanban con drag & drop
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { icon } from '../icons.js';
|
|
6
|
+
import * as state from '../state.js';
|
|
7
|
+
import * as api from '../api.js';
|
|
8
|
+
import * as router from '../router.js';
|
|
9
|
+
import { flash } from './flash.js';
|
|
10
|
+
import { esc, debounce } from '../utils.js';
|
|
11
|
+
import { t } from '../i18n.js';
|
|
12
|
+
|
|
13
|
+
const COLUMNS = [
|
|
14
|
+
{ id: 'pending', label: 'Pending' },
|
|
15
|
+
{ id: 'in_progress', label: 'In Progress' },
|
|
16
|
+
{ id: 'in_review', label: 'In Review' },
|
|
17
|
+
{ id: 'blocked', label: 'Blocked' },
|
|
18
|
+
{ id: 'completed', label: 'Completed' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
let _dragTaskId = null;
|
|
22
|
+
|
|
23
|
+
export async function render() {
|
|
24
|
+
const payload = state.getPayload();
|
|
25
|
+
if (!payload) return `<div class="empty-state" style="margin:3rem">${t('ui.board.noData', {}, 'No project data.')}</div>`;
|
|
26
|
+
|
|
27
|
+
const tasks = _filterTasks(payload.derived.tasks);
|
|
28
|
+
const hasCancelled = tasks.some(t => t.status === 'cancelled');
|
|
29
|
+
const columns = hasCancelled
|
|
30
|
+
? [...COLUMNS, { id: 'cancelled', label: t('status.cancelled', {}, 'Cancelled') }]
|
|
31
|
+
: COLUMNS.map(col => ({ ...col, label: t(`status.${col.id}`, {}, col.label) }));
|
|
32
|
+
|
|
33
|
+
const html = `
|
|
34
|
+
<div class="view-enter">
|
|
35
|
+
<div class="section-header">
|
|
36
|
+
<div class="section-header-left">
|
|
37
|
+
<p class="eyebrow">${t('ui.board.eyebrow', {}, 'Board')}</p>
|
|
38
|
+
<h2>${t('ui.board.title', {}, 'Operational board')}</h2>
|
|
39
|
+
</div>
|
|
40
|
+
<div style="display:flex;gap:var(--space-2)">
|
|
41
|
+
<button class="btn btn-ghost btn-sm" id="board-filter-done" type="button">
|
|
42
|
+
${icon('check', 14)} ${_showCompleted() ? t('ui.board.hideCompleted', {}, 'Hide completed') : t('ui.board.showCompleted', {}, 'Show completed')}
|
|
43
|
+
</button>
|
|
44
|
+
<button class="btn btn-primary btn-sm" id="new-task-btn" type="button" aria-label="${t('ui.board.newTask', {}, 'Create new task')}">
|
|
45
|
+
${icon('plus', 14)} ${t('ui.tasks.new', {}, 'New task')}
|
|
46
|
+
</button>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div class="board-grid" id="board" aria-label="${t('ui.board.aria', {}, 'Task board by status')}" role="region">
|
|
51
|
+
${columns.map(col => {
|
|
52
|
+
const colTasks = tasks.filter(t => t.status === col.id);
|
|
53
|
+
return _renderColumn(col, colTasks);
|
|
54
|
+
}).join('')}
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
setTimeout(() => _bindEvents(), 0);
|
|
60
|
+
return html;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function _filterTasks(tasks) {
|
|
64
|
+
let list = tasks;
|
|
65
|
+
const query = state.get('searchQuery')?.toLowerCase();
|
|
66
|
+
if (query) {
|
|
67
|
+
list = list.filter(t =>
|
|
68
|
+
t.title.toLowerCase().includes(query) ||
|
|
69
|
+
t.id.toLowerCase().includes(query) ||
|
|
70
|
+
(t.summary || '').toLowerCase().includes(query)
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (!_showCompleted()) {
|
|
74
|
+
list = list.filter(t => t.status !== 'completed' && t.status !== 'cancelled');
|
|
75
|
+
}
|
|
76
|
+
return list;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function _showCompleted() {
|
|
80
|
+
return sessionStorage.getItem('board-show-completed') === 'true';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function _renderColumn(col, tasks) {
|
|
84
|
+
return `
|
|
85
|
+
<section class="board-column col-${col.id}" data-status="${col.id}" aria-label="${t('ui.board.column', { label: col.label }, `Column ${col.label}`)}">
|
|
86
|
+
<div class="board-column-header">
|
|
87
|
+
<h3 class="board-column-title" id="col-${col.id}">
|
|
88
|
+
<span class="board-column-dot" aria-hidden="true"></span>
|
|
89
|
+
${esc(col.label)}
|
|
90
|
+
</h3>
|
|
91
|
+
<span class="board-column-count" aria-label="${t('ui.board.tasksCount', { count: tasks.length }, `${tasks.length} tasks`)}">${tasks.length}</span>
|
|
92
|
+
</div>
|
|
93
|
+
<div class="board-column-body" aria-labelledby="col-${col.id}" role="list">
|
|
94
|
+
${tasks.map(t => _renderCard(t)).join('')}
|
|
95
|
+
${tasks.length === 0
|
|
96
|
+
? `<div class="empty-state" style="padding:var(--space-5);min-height:80px;border-style:dashed">${t('ui.board.noTasks', {}, 'No tasks')}</div>`
|
|
97
|
+
: ''}
|
|
98
|
+
</div>
|
|
99
|
+
</section>
|
|
100
|
+
`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function _renderCard(task) {
|
|
104
|
+
const isSelected = task.id === state.get('selectedTaskId');
|
|
105
|
+
const statusLabels = state.getStatusLabels();
|
|
106
|
+
const phases = state.getPhases();
|
|
107
|
+
const phaseInfo = phases.find(p => p.id === task.phase);
|
|
108
|
+
|
|
109
|
+
const priorityVariant = { P0: 'danger', P1: 'warning', P2: 'accent', P3: 'muted' };
|
|
110
|
+
|
|
111
|
+
return `
|
|
112
|
+
<article
|
|
113
|
+
class="task-card ${isSelected ? 'is-selected' : ''}"
|
|
114
|
+
data-task-id="${esc(task.id)}"
|
|
115
|
+
data-status="${esc(task.status)}"
|
|
116
|
+
draggable="true"
|
|
117
|
+
role="listitem"
|
|
118
|
+
tabindex="0"
|
|
119
|
+
aria-label="${esc(task.title)}, ${statusLabels[task.status] || task.status}, ${t('ui.board.priority', { priority: task.priority }, `priority ${task.priority}`)}"
|
|
120
|
+
aria-selected="${isSelected}"
|
|
121
|
+
>
|
|
122
|
+
<strong class="task-card-title">${esc(task.title)}</strong>
|
|
123
|
+
<span class="task-card-id">${esc(task.id)}</span>
|
|
124
|
+
<p class="task-card-summary">${esc(task.summary || t('ui.board.noDescription', {}, 'No description.'))}</p>
|
|
125
|
+
<div class="task-card-meta">
|
|
126
|
+
<span class="badge badge-${priorityVariant[task.priority] || 'muted'}">${esc(task.priority)}</span>
|
|
127
|
+
<span class="badge badge-muted">${esc(phaseInfo?.label || task.phase)}</span>
|
|
128
|
+
${task.stream ? `<span class="badge badge-muted">${esc(task.stream)}</span>` : ''}
|
|
129
|
+
${task.blocker ? `<span class="badge badge-danger" title="${esc(task.blocker)}">${icon('alertTriangle', 10)} ${t('status.blocked', {}, 'Blocked')}</span>` : ''}
|
|
130
|
+
</div>
|
|
131
|
+
</article>
|
|
132
|
+
`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function _bindEvents() {
|
|
136
|
+
const board = document.getElementById('board');
|
|
137
|
+
if (!board) return;
|
|
138
|
+
|
|
139
|
+
// Clic en task card → seleccionar y navegar a tasks
|
|
140
|
+
board.addEventListener('click', e => {
|
|
141
|
+
const card = e.target.closest('.task-card');
|
|
142
|
+
if (!card) return;
|
|
143
|
+
|
|
144
|
+
const id = card.dataset.taskId;
|
|
145
|
+
state.update('selectedTaskId', id);
|
|
146
|
+
|
|
147
|
+
// Navegar al editor si hubo doble clic; si un clic simple, solo marcar
|
|
148
|
+
if (e.detail === 2) {
|
|
149
|
+
router.navigate('tasks');
|
|
150
|
+
} else {
|
|
151
|
+
// Resaltar la card seleccionada sin re-render completo
|
|
152
|
+
board.querySelectorAll('.task-card').forEach(c => {
|
|
153
|
+
c.classList.toggle('is-selected', c.dataset.taskId === id);
|
|
154
|
+
c.setAttribute('aria-selected', c.dataset.taskId === id ? 'true' : 'false');
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Teclado en cards (Enter = seleccionar, Space = toggle)
|
|
160
|
+
board.addEventListener('keydown', e => {
|
|
161
|
+
const card = e.target.closest('.task-card');
|
|
162
|
+
if (!card) return;
|
|
163
|
+
if (e.key === 'Enter') {
|
|
164
|
+
state.update('selectedTaskId', card.dataset.taskId);
|
|
165
|
+
router.navigate('tasks');
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Toggle mostrar completadas
|
|
170
|
+
document.getElementById('board-filter-done')?.addEventListener('click', () => {
|
|
171
|
+
const current = _showCompleted();
|
|
172
|
+
sessionStorage.setItem('board-show-completed', current ? 'false' : 'true');
|
|
173
|
+
router.refresh();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Nueva tarea
|
|
177
|
+
document.getElementById('new-task-btn')?.addEventListener('click', () => {
|
|
178
|
+
state.update('selectedTaskId', null);
|
|
179
|
+
router.navigate('tasks');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Drag & drop
|
|
183
|
+
_bindDragDrop(board);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function _bindDragDrop(board) {
|
|
187
|
+
// Dragstart
|
|
188
|
+
board.addEventListener('dragstart', e => {
|
|
189
|
+
const card = e.target.closest('.task-card');
|
|
190
|
+
if (!card) return;
|
|
191
|
+
_dragTaskId = card.dataset.taskId;
|
|
192
|
+
card.classList.add('is-dragging');
|
|
193
|
+
e.dataTransfer.setData('text/plain', _dragTaskId);
|
|
194
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
board.addEventListener('dragend', e => {
|
|
198
|
+
const card = e.target.closest('.task-card');
|
|
199
|
+
card?.classList.remove('is-dragging');
|
|
200
|
+
_dragTaskId = null;
|
|
201
|
+
board.querySelectorAll('.board-column').forEach(col => col.classList.remove('is-drop-target'));
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
board.addEventListener('dragover', e => {
|
|
205
|
+
e.preventDefault();
|
|
206
|
+
const col = e.target.closest('.board-column');
|
|
207
|
+
if (!col) return;
|
|
208
|
+
e.dataTransfer.dropEffect = 'move';
|
|
209
|
+
board.querySelectorAll('.board-column').forEach(c => c.classList.remove('is-drop-target'));
|
|
210
|
+
col.classList.add('is-drop-target');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
board.addEventListener('dragleave', e => {
|
|
214
|
+
const col = e.target.closest('.board-column');
|
|
215
|
+
if (!col) return;
|
|
216
|
+
// Solo eliminar si salimos de la columna, no de un hijo
|
|
217
|
+
if (!col.contains(e.relatedTarget)) {
|
|
218
|
+
col.classList.remove('is-drop-target');
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
board.addEventListener('drop', async e => {
|
|
223
|
+
e.preventDefault();
|
|
224
|
+
const col = e.target.closest('.board-column');
|
|
225
|
+
if (!col) return;
|
|
226
|
+
col.classList.remove('is-drop-target');
|
|
227
|
+
|
|
228
|
+
const taskId = e.dataTransfer.getData('text/plain') || _dragTaskId;
|
|
229
|
+
if (!taskId) return;
|
|
230
|
+
|
|
231
|
+
const newStatus = col.dataset.status;
|
|
232
|
+
const task = state.getPayload()?.derived?.tasks?.find(t => t.id === taskId);
|
|
233
|
+
if (!task || task.status === newStatus) return;
|
|
234
|
+
|
|
235
|
+
const statusToAction = {
|
|
236
|
+
pending: 'pending',
|
|
237
|
+
in_progress: 'start',
|
|
238
|
+
in_review: 'review',
|
|
239
|
+
blocked: 'block',
|
|
240
|
+
completed: 'complete',
|
|
241
|
+
cancelled: 'cancel',
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const action = statusToAction[newStatus];
|
|
245
|
+
if (!action) return;
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
await api.taskAction(taskId, action, t('ui.board.movedFromBoard', { status: newStatus }, `Moved to ${newStatus} from the board.`));
|
|
249
|
+
flash(t('ui.board.movedSuccess', { status: newStatus }, `Task moved to ${newStatus}.`), 'success');
|
|
250
|
+
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
251
|
+
} catch (err) {
|
|
252
|
+
flash(err.message, 'error');
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|