trackops 2.0.3 → 2.0.5

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 (103) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +695 -402
  3. package/bin/trackops.js +116 -116
  4. package/lib/config.js +326 -326
  5. package/lib/control.js +208 -208
  6. package/lib/env.js +244 -244
  7. package/lib/init.js +325 -325
  8. package/lib/locale.js +24 -0
  9. package/lib/opera-bootstrap.js +941 -874
  10. package/lib/opera.js +494 -477
  11. package/lib/preferences.js +74 -74
  12. package/lib/registry.js +214 -196
  13. package/lib/release.js +56 -56
  14. package/lib/runtime-state.js +144 -144
  15. package/lib/server.js +312 -207
  16. package/lib/skills.js +74 -57
  17. package/lib/workspace.js +260 -260
  18. package/locales/en.json +192 -166
  19. package/locales/es.json +192 -166
  20. package/package.json +61 -58
  21. package/scripts/postinstall-locale.js +21 -21
  22. package/scripts/skills-marketplace-smoke.js +124 -124
  23. package/scripts/smoke-tests.js +558 -554
  24. package/scripts/sync-skill-version.js +21 -21
  25. package/scripts/validate-skill.js +103 -103
  26. package/skills/trackops/SKILL.md +126 -122
  27. package/skills/trackops/agents/openai.yaml +7 -7
  28. package/skills/trackops/locales/en/SKILL.md +126 -122
  29. package/skills/trackops/locales/en/references/activation.md +94 -75
  30. package/skills/trackops/locales/en/references/troubleshooting.md +73 -55
  31. package/skills/trackops/locales/en/references/workflow.md +55 -32
  32. package/skills/trackops/references/activation.md +94 -75
  33. package/skills/trackops/references/troubleshooting.md +73 -55
  34. package/skills/trackops/references/workflow.md +55 -32
  35. package/skills/trackops/skill.json +29 -29
  36. package/templates/hooks/post-checkout +2 -2
  37. package/templates/hooks/post-commit +2 -2
  38. package/templates/hooks/post-merge +2 -2
  39. package/templates/opera/agent.md +28 -27
  40. package/templates/opera/architecture/dependency-graph.md +24 -24
  41. package/templates/opera/architecture/runtime-automation.md +24 -24
  42. package/templates/opera/architecture/runtime-operations.md +34 -34
  43. package/templates/opera/en/agent.md +22 -21
  44. package/templates/opera/en/architecture/dependency-graph.md +24 -24
  45. package/templates/opera/en/architecture/runtime-automation.md +24 -24
  46. package/templates/opera/en/architecture/runtime-operations.md +34 -34
  47. package/templates/opera/en/reviews/delivery-audit.md +18 -18
  48. package/templates/opera/en/reviews/integration-audit.md +18 -18
  49. package/templates/opera/en/router.md +24 -19
  50. package/templates/opera/references/autonomy-and-recovery.md +117 -117
  51. package/templates/opera/references/opera-cycle.md +193 -193
  52. package/templates/opera/registry.md +28 -28
  53. package/templates/opera/reviews/delivery-audit.md +18 -18
  54. package/templates/opera/reviews/integration-audit.md +18 -18
  55. package/templates/opera/router.md +54 -49
  56. package/templates/skills/changelog-updater/SKILL.md +69 -69
  57. package/templates/skills/commiter/SKILL.md +99 -99
  58. package/templates/skills/opera-contract-auditor/SKILL.md +38 -38
  59. package/templates/skills/opera-contract-auditor/locales/en/SKILL.md +38 -38
  60. package/templates/skills/opera-policy-guard/SKILL.md +26 -26
  61. package/templates/skills/opera-policy-guard/locales/en/SKILL.md +26 -26
  62. package/templates/skills/opera-skill/SKILL.md +279 -0
  63. package/templates/skills/opera-skill/locales/en/SKILL.md +279 -0
  64. package/templates/skills/opera-skill/locales/en/references/phase-dod.md +138 -0
  65. package/templates/skills/opera-skill/references/phase-dod.md +138 -0
  66. package/templates/skills/project-starter-skill/SKILL.md +150 -131
  67. package/templates/skills/project-starter-skill/locales/en/SKILL.md +143 -105
  68. package/templates/skills/project-starter-skill/references/opera-cycle.md +195 -193
  69. package/ui/css/base.css +284 -266
  70. package/ui/css/charts.css +425 -327
  71. package/ui/css/components.css +1107 -570
  72. package/ui/css/onboarding.css +133 -0
  73. package/ui/css/panels.css +345 -406
  74. package/ui/css/terminal.css +125 -0
  75. package/ui/css/timeline.css +58 -0
  76. package/ui/css/tokens.css +284 -227
  77. package/ui/favicon.svg +5 -5
  78. package/ui/index.html +99 -96
  79. package/ui/js/api.js +49 -13
  80. package/ui/js/app.js +28 -32
  81. package/ui/js/charts.js +526 -0
  82. package/ui/js/console-logger.js +172 -172
  83. package/ui/js/filters.js +247 -0
  84. package/ui/js/icons.js +129 -104
  85. package/ui/js/keyboard.js +229 -0
  86. package/ui/js/onboarding.js +33 -42
  87. package/ui/js/router.js +142 -125
  88. package/ui/js/theme.js +100 -100
  89. package/ui/js/time-tracker.js +248 -248
  90. package/ui/js/views/board.js +84 -114
  91. package/ui/js/views/dashboard.js +870 -0
  92. package/ui/js/views/flash.js +47 -47
  93. package/ui/js/views/projects.js +745 -0
  94. package/ui/js/views/scrum.js +476 -0
  95. package/ui/js/views/settings.js +153 -203
  96. package/ui/js/views/sidebar.js +37 -31
  97. package/ui/js/views/tasks.js +218 -101
  98. package/ui/js/views/timeline.js +265 -0
  99. package/ui/js/views/topbar.js +94 -107
  100. package/ui/app.js +0 -950
  101. package/ui/js/views/insights.js +0 -340
  102. package/ui/js/views/overview.js +0 -369
  103. package/ui/styles.css +0 -688
@@ -1,97 +1,121 @@
1
1
  /**
2
- * board.js — Tablero Kanban con drag & drop
2
+ * board.js — Headless Kanban module (embeddable inside tasks.js)
3
+ *
4
+ * Exports:
5
+ * renderKanban(tasks, phases, opts) — returns kanban board HTML (columns + cards only)
6
+ * bindKanbanEvents() — binds drag-drop, card click/selection, task actions
3
7
  */
4
8
 
5
9
  import { icon } from '../icons.js';
6
10
  import * as state from '../state.js';
7
11
  import * as api from '../api.js';
8
- import * as router from '../router.js';
9
12
  import { flash } from './flash.js';
10
- import { esc, debounce } from '../utils.js';
13
+ import { esc } from '../utils.js';
11
14
  import { t } from '../i18n.js';
12
15
 
13
16
  const COLUMNS = [
14
17
  { id: 'pending', label: 'Pending' },
15
18
  { id: 'in_progress', label: 'In Progress' },
16
- { id: 'in_review', label: 'In Review' },
17
- { id: 'blocked', label: 'Blocked' },
18
- { id: 'completed', label: 'Completed' },
19
+ { id: 'in_review', label: 'In Review' },
20
+ { id: 'blocked', label: 'Blocked' },
21
+ { id: 'completed', label: 'Completed' },
19
22
  ];
20
23
 
21
24
  let _dragTaskId = null;
22
25
 
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
+ * Render the kanban board HTML (columns + cards only).
28
+ *
29
+ * @param {Array} tasks — pre-filtered task list
30
+ * @param {Array} phases — array of phase objects ({ id, label, ... })
31
+ * @param {Object} opts — { showCompleted, showCancelled, wipLimits }
32
+ * wipLimits is an optional map: { status: number }
33
+ * @returns {string} HTML string for the board grid
34
+ */
35
+ export function renderKanban(tasks, phases, opts = {}) {
36
+ const { showCompleted = true, showCancelled = false, wipLimits = {} } = opts;
37
+
38
+ // Filter out completed / cancelled unless opted-in
39
+ let visibleTasks = tasks;
40
+ if (!showCompleted) {
41
+ visibleTasks = visibleTasks.filter(t => t.status !== 'completed');
42
+ }
43
+ if (!showCancelled) {
44
+ visibleTasks = visibleTasks.filter(t => t.status !== 'cancelled');
45
+ }
26
46
 
27
- const tasks = _filterTasks(payload.derived.tasks);
28
- const hasCancelled = tasks.some(t => t.status === 'cancelled');
47
+ // Build column set
48
+ const hasCancelled = showCancelled && visibleTasks.some(t => t.status === 'cancelled');
29
49
  const columns = hasCancelled
30
50
  ? [...COLUMNS, { id: 'cancelled', label: t('status.cancelled', {}, 'Cancelled') }]
31
51
  : COLUMNS.map(col => ({ ...col, label: t(`status.${col.id}`, {}, col.label) }));
32
52
 
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>
53
+ return `
54
+ <div class="board-grid" id="board" aria-label="${t('ui.board.aria', {}, 'Task board by status')}" role="region">
55
+ ${columns.map(col => {
56
+ const colTasks = visibleTasks.filter(t => t.status === col.id);
57
+ const wipLimit = wipLimits[col.id] ?? null;
58
+ return _renderColumn(col, colTasks, phases, wipLimit);
59
+ }).join('')}
56
60
  </div>
57
61
  `;
58
-
59
- setTimeout(() => _bindEvents(), 0);
60
- return html;
61
62
  }
62
63
 
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
- }
64
+ /**
65
+ * Bind kanban-specific events: drag-drop, card click/selection, task actions.
66
+ * Call this after the board HTML has been inserted into the DOM.
67
+ */
68
+ export function bindKanbanEvents() {
69
+ const board = document.getElementById('board');
70
+ if (!board) return;
71
+
72
+ // Click on task card — select and highlight
73
+ board.addEventListener('click', e => {
74
+ const card = e.target.closest('.task-card');
75
+ if (!card) return;
76
+
77
+ const id = card.dataset.taskId;
78
+ state.update('selectedTaskId', id);
79
+
80
+ // Highlight the selected card without a full re-render
81
+ board.querySelectorAll('.task-card').forEach(c => {
82
+ c.classList.toggle('is-selected', c.dataset.taskId === id);
83
+ c.setAttribute('aria-selected', c.dataset.taskId === id ? 'true' : 'false');
84
+ });
85
+ });
86
+
87
+ // Keyboard on cards (Enter = select)
88
+ board.addEventListener('keydown', e => {
89
+ const card = e.target.closest('.task-card');
90
+ if (!card) return;
91
+ if (e.key === 'Enter') {
92
+ state.update('selectedTaskId', card.dataset.taskId);
93
+ }
94
+ });
78
95
 
79
- function _showCompleted() {
80
- return sessionStorage.getItem('board-show-completed') === 'true';
96
+ // Drag & drop
97
+ _bindDragDrop(board);
81
98
  }
82
99
 
83
- function _renderColumn(col, tasks) {
100
+ /* ------------------------------------------------------------------ */
101
+ /* Internal helpers */
102
+ /* ------------------------------------------------------------------ */
103
+
104
+ function _renderColumn(col, tasks, phases, wipLimit) {
105
+ const wipExceeded = wipLimit != null && tasks.length > wipLimit;
106
+ const wipAttr = wipLimit != null ? ` data-wip-limit="${wipLimit}"` : '';
107
+
84
108
  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">
109
+ <section class="board-column col-${col.id}" data-status="${col.id}"${wipAttr} aria-label="${t('ui.board.column', { label: col.label }, `Column ${col.label}`)}">
110
+ <div class="board-column-header${wipExceeded ? ' wip-exceeded' : ''}">
87
111
  <h3 class="board-column-title" id="col-${col.id}">
88
112
  <span class="board-column-dot" aria-hidden="true"></span>
89
113
  ${esc(col.label)}
90
114
  </h3>
91
- <span class="board-column-count" aria-label="${t('ui.board.tasksCount', { count: tasks.length }, `${tasks.length} tasks`)}">${tasks.length}</span>
115
+ <span class="board-column-count" aria-label="${t('ui.board.tasksCount', { count: tasks.length }, `${tasks.length} tasks`)}">${tasks.length}${wipLimit != null ? `/${wipLimit}` : ''}</span>
92
116
  </div>
93
117
  <div class="board-column-body" aria-labelledby="col-${col.id}" role="list">
94
- ${tasks.map(t => _renderCard(t)).join('')}
118
+ ${tasks.map(task => _renderCard(task, phases)).join('')}
95
119
  ${tasks.length === 0
96
120
  ? `<div class="empty-state" style="padding:var(--space-5);min-height:80px;border-style:dashed">${t('ui.board.noTasks', {}, 'No tasks')}</div>`
97
121
  : ''}
@@ -100,10 +124,9 @@ function _renderColumn(col, tasks) {
100
124
  `;
101
125
  }
102
126
 
103
- function _renderCard(task) {
127
+ function _renderCard(task, phases) {
104
128
  const isSelected = task.id === state.get('selectedTaskId');
105
129
  const statusLabels = state.getStatusLabels();
106
- const phases = state.getPhases();
107
130
  const phaseInfo = phases.find(p => p.id === task.phase);
108
131
 
109
132
  const priorityVariant = { P0: 'danger', P1: 'warning', P2: 'accent', P3: 'muted' };
@@ -132,59 +155,7 @@ function _renderCard(task) {
132
155
  `;
133
156
  }
134
157
 
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
158
  function _bindDragDrop(board) {
187
- // Dragstart
188
159
  board.addEventListener('dragstart', e => {
189
160
  const card = e.target.closest('.task-card');
190
161
  if (!card) return;
@@ -213,7 +184,6 @@ function _bindDragDrop(board) {
213
184
  board.addEventListener('dragleave', e => {
214
185
  const col = e.target.closest('.board-column');
215
186
  if (!col) return;
216
- // Solo eliminar si salimos de la columna, no de un hijo
217
187
  if (!col.contains(e.relatedTarget)) {
218
188
  col.classList.remove('is-drop-target');
219
189
  }