mindforge-cc 11.9.4 → 11.9.6
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/.mindforge/config.json +1 -1
- package/CHANGELOG.md +267 -8
- package/MINDFORGE.md +2 -2
- package/README.md +32 -11
- package/RELEASENOTES.md +167 -0
- package/SECURITY.md +2 -2
- package/bin/browser/browser-daemon.js +84 -48
- package/bin/browser/daemon-auth.js +36 -0
- package/bin/browser/daemon-manager.js +53 -22
- package/bin/dashboard/frontend/app.js +206 -101
- package/bin/skills-builder/learn-cli.js +26 -14
- package/bin/utils/readiness-gate.js +19 -3
- package/changelogs/v11.9.4.md +31 -8
- package/changelogs/v11.9.5.md +104 -0
- package/changelogs/v11.9.6.md +135 -0
- package/docs/References/commands.md +13 -8
- package/docs/References/decimal-phase-calculation.md +5 -5
- package/docs/References/git-integration.md +4 -4
- package/docs/References/git-planning-commit.md +2 -2
- package/docs/References/model-profile-resolution.md +1 -1
- package/docs/faq.md +5 -4
- package/docs/getting-started.md +3 -3
- package/docs/sdk-reference.md +17 -11
- package/docs/troubleshooting.md +1 -1
- package/docs/user-guide.md +4 -4
- package/examples/sdk-integration/index.js +1 -1
- package/package.json +7 -3
|
@@ -33,21 +33,33 @@
|
|
|
33
33
|
|
|
34
34
|
// ── State ─────────────────────────────────────────────────────────────────
|
|
35
35
|
let currentApprovals = [];
|
|
36
|
-
let state = {
|
|
36
|
+
let state = { sessions: [] }; // replaced wholesale by refreshData() with the /api/metrics response
|
|
37
37
|
|
|
38
38
|
function showPage(id) {
|
|
39
|
-
document
|
|
40
|
-
|
|
39
|
+
document
|
|
40
|
+
.querySelectorAll('.page')
|
|
41
|
+
.forEach((p) => p.classList.remove('active'));
|
|
42
|
+
document
|
|
43
|
+
.querySelectorAll('.tab')
|
|
44
|
+
.forEach((t) => t.classList.remove('active'));
|
|
41
45
|
document.getElementById(id).classList.add('active');
|
|
42
|
-
const tab = Array.from(document.querySelectorAll('.tab')).find(
|
|
46
|
+
const tab = Array.from(document.querySelectorAll('.tab')).find(
|
|
47
|
+
(t) => t.innerText.toLowerCase() === id.toLowerCase(),
|
|
48
|
+
);
|
|
43
49
|
if (tab) tab.classList.add('active');
|
|
44
|
-
|
|
50
|
+
|
|
45
51
|
if (id === 'metrics') drawCharts();
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
function escapeHtml(str) {
|
|
49
55
|
if (!str) return '';
|
|
50
|
-
return String(str).replace(
|
|
56
|
+
return String(str).replace(
|
|
57
|
+
/[&<>"']/g,
|
|
58
|
+
(m) =>
|
|
59
|
+
({ '&': '&', '<': '<', '>': '>', '"': '"', '\'': ''' })[
|
|
60
|
+
m
|
|
61
|
+
],
|
|
62
|
+
);
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
// ── WebSocket / SSE ───────────────────────────────────────────────────────
|
|
@@ -95,9 +107,11 @@ async function refreshData() {
|
|
|
95
107
|
refreshTeam();
|
|
96
108
|
refreshRevOps();
|
|
97
109
|
if (document.getElementById('temporal').classList.contains('active')) {
|
|
98
|
-
|
|
110
|
+
refreshTemporal();
|
|
99
111
|
}
|
|
100
|
-
} catch(e) {
|
|
112
|
+
} catch (e) {
|
|
113
|
+
console.error('Refresh fail', e);
|
|
114
|
+
}
|
|
101
115
|
}
|
|
102
116
|
|
|
103
117
|
async function refreshRevOps() {
|
|
@@ -105,9 +119,11 @@ async function refreshRevOps() {
|
|
|
105
119
|
const res = await fetch('/api/revops/overview');
|
|
106
120
|
const data = await res.json();
|
|
107
121
|
if (data.success) {
|
|
108
|
-
|
|
122
|
+
updateRevOpsUI(data);
|
|
109
123
|
}
|
|
110
|
-
} catch {
|
|
124
|
+
} catch {
|
|
125
|
+
/* one panel failing must not abort the rest of the refresh; it keeps its last value */
|
|
126
|
+
}
|
|
111
127
|
}
|
|
112
128
|
|
|
113
129
|
let temporalHistory = [];
|
|
@@ -117,7 +133,9 @@ async function refreshTemporal() {
|
|
|
117
133
|
const data = await res.json();
|
|
118
134
|
temporalHistory = data;
|
|
119
135
|
renderTemporalTimeline();
|
|
120
|
-
} catch {
|
|
136
|
+
} catch {
|
|
137
|
+
/* one panel failing must not abort the rest of the refresh; it keeps its last value */
|
|
138
|
+
}
|
|
121
139
|
}
|
|
122
140
|
|
|
123
141
|
async function refreshApprovals() {
|
|
@@ -126,16 +144,25 @@ async function refreshApprovals() {
|
|
|
126
144
|
const data = await res.json();
|
|
127
145
|
currentApprovals = data;
|
|
128
146
|
renderApprovals();
|
|
129
|
-
} catch {
|
|
147
|
+
} catch {
|
|
148
|
+
/* one panel failing must not abort the rest of the refresh; it keeps its last value */
|
|
149
|
+
}
|
|
130
150
|
}
|
|
131
151
|
|
|
132
152
|
async function refreshMemory() {
|
|
133
153
|
try {
|
|
134
154
|
const res = await fetch('/api/memory');
|
|
155
|
+
// Metrics.getMemory() returns { entries, total } — a flat, confidence-sorted knowledge-base
|
|
156
|
+
// list, not a graph. This panel used to read `data.graph`/`data.count`, fields that endpoint
|
|
157
|
+
// has never produced, so it always rendered "{}" and "0 Items Indexed".
|
|
135
158
|
const data = await res.json();
|
|
136
|
-
document.getElementById('memory-map').innerHTML =
|
|
137
|
-
|
|
138
|
-
|
|
159
|
+
document.getElementById('memory-map').innerHTML =
|
|
160
|
+
`<pre>${escapeHtml(JSON.stringify(data.entries || [], null, 2))}</pre>`;
|
|
161
|
+
document.getElementById('memory-stats-list').innerHTML =
|
|
162
|
+
`<div class="stat-value">${data.total || 0} <span class="stat-unit">Items Indexed</span></div>`;
|
|
163
|
+
} catch {
|
|
164
|
+
/* one panel failing must not abort the rest of the refresh; it keeps its last value */
|
|
165
|
+
}
|
|
139
166
|
}
|
|
140
167
|
|
|
141
168
|
async function refreshTeam() {
|
|
@@ -143,7 +170,9 @@ async function refreshTeam() {
|
|
|
143
170
|
const res = await fetch('/api/team');
|
|
144
171
|
const data = await res.json();
|
|
145
172
|
renderTeam(data);
|
|
146
|
-
} catch {
|
|
173
|
+
} catch {
|
|
174
|
+
/* one panel failing must not abort the rest of the refresh; it keeps its last value */
|
|
175
|
+
}
|
|
147
176
|
}
|
|
148
177
|
|
|
149
178
|
// decide() and submitDecision() are removed along with POST /api/approve/:id. They sent
|
|
@@ -153,25 +182,32 @@ async function refreshTeam() {
|
|
|
153
182
|
// by bin/governance/approve.js, which takes identity from git and fails closed without a GPG
|
|
154
183
|
// key unless MINDFORGE_ALLOW_UNVERIFIED_APPROVAL=1.
|
|
155
184
|
|
|
156
|
-
|
|
157
185
|
function closeConfirm() {
|
|
158
186
|
document.getElementById('confirm-overlay').style.display = 'none';
|
|
159
187
|
}
|
|
160
188
|
|
|
161
189
|
// ── UI Rendering ──────────────────────────────────────────────────────────
|
|
162
190
|
function updateStatusUI(data) {
|
|
163
|
-
document.getElementById('project-name').textContent =
|
|
191
|
+
document.getElementById('project-name').textContent =
|
|
192
|
+
`PROJ: ${data.project_name || 'UNSET'}`;
|
|
164
193
|
document.getElementById('stat-phase').textContent = data.phase || '0';
|
|
165
|
-
document.getElementById('stat-status').textContent = (
|
|
166
|
-
|
|
167
|
-
|
|
194
|
+
document.getElementById('stat-status').textContent = (
|
|
195
|
+
data.auto_status || 'IDLE'
|
|
196
|
+
).toUpperCase();
|
|
197
|
+
document.getElementById('stat-tasks').textContent =
|
|
198
|
+
`${data.tasks_completed || 0}/${data.tasks_total || 0}`;
|
|
199
|
+
document.getElementById('stat-elapsed').textContent = data.elapsed_ms
|
|
200
|
+
? (data.elapsed_ms / 1000).toFixed(1) + 's'
|
|
201
|
+
: '0s';
|
|
168
202
|
}
|
|
169
203
|
|
|
170
204
|
function updateMetricsUI(data) {
|
|
171
205
|
// /api/metrics returns { sessions, avg_quality, avg_cost_usd, ... } — see
|
|
172
206
|
// bin/dashboard/metrics-aggregator.js getMetrics(). It has never returned
|
|
173
207
|
// `costs` or `quality`, so these tiles used to render "$0.0000" and "NaN".
|
|
174
|
-
document.getElementById('stat-avg-quality').textContent = (
|
|
208
|
+
document.getElementById('stat-avg-quality').textContent = (
|
|
209
|
+
data.avg_quality ?? 0
|
|
210
|
+
).toFixed(1);
|
|
175
211
|
}
|
|
176
212
|
|
|
177
213
|
// Cumulative spend comes from the usage ledger via /api/costs (already wired
|
|
@@ -184,14 +220,22 @@ async function refreshCosts() {
|
|
|
184
220
|
// and genuine zero spend look identical under the "Cost (7d)" label.
|
|
185
221
|
if (!res.ok) throw new Error(`/api/costs ${res.status}`);
|
|
186
222
|
const data = await res.json();
|
|
187
|
-
document.getElementById('stat-total-cost').textContent =
|
|
188
|
-
|
|
223
|
+
document.getElementById('stat-total-cost').textContent =
|
|
224
|
+
`${(data.total_usd ?? 0).toFixed(2)}`;
|
|
225
|
+
} catch (e) {
|
|
226
|
+
console.error('Cost refresh failed', e);
|
|
227
|
+
}
|
|
189
228
|
}
|
|
190
229
|
|
|
191
230
|
function appendAuditEvent(data) {
|
|
192
231
|
const div = document.createElement('div');
|
|
193
232
|
div.className = `event type-${data.event}`;
|
|
194
|
-
const time = new Date(data.timestamp).toLocaleTimeString([], {
|
|
233
|
+
const time = new Date(data.timestamp).toLocaleTimeString([], {
|
|
234
|
+
hour12: false,
|
|
235
|
+
hour: '2-digit',
|
|
236
|
+
minute: '2-digit',
|
|
237
|
+
second: '2-digit',
|
|
238
|
+
});
|
|
195
239
|
div.innerHTML = `<span class="event-time">${time}</span><span class="event-type type-${data.event}">${data.event}</span><span class="event-msg">${escapeHtml(data.message || data.task || '')}</span>`;
|
|
196
240
|
feed.prepend(div);
|
|
197
241
|
if (feed.children.length > 200) feed.lastChild.remove();
|
|
@@ -208,22 +252,33 @@ function renderApprovals() {
|
|
|
208
252
|
const list = document.getElementById('approval-list');
|
|
209
253
|
const records = Array.isArray(currentApprovals) ? currentApprovals : [];
|
|
210
254
|
if (records.length === 0) {
|
|
211
|
-
list.innerHTML =
|
|
212
|
-
|
|
213
|
-
|
|
255
|
+
list.innerHTML =
|
|
256
|
+
'<div style="text-align:center; padding: 40px; color:var(--muted)">' +
|
|
257
|
+
'No approval records. Tier-3 review is a recorded human acknowledgement; ' +
|
|
258
|
+
'authorization is enforced by branch protection, not from here.</div>';
|
|
214
259
|
return;
|
|
215
260
|
}
|
|
216
261
|
|
|
217
|
-
const colour = {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
262
|
+
const colour = {
|
|
263
|
+
valid: 'var(--green)',
|
|
264
|
+
stale: 'var(--purple)',
|
|
265
|
+
corrupt: 'var(--red)',
|
|
266
|
+
};
|
|
267
|
+
list.innerHTML = records
|
|
268
|
+
.map((a) => {
|
|
269
|
+
const hrs = a.hours_remaining;
|
|
270
|
+
const life =
|
|
271
|
+
hrs === null || hrs === undefined
|
|
272
|
+
? ''
|
|
273
|
+
: hrs > 0
|
|
274
|
+
? `${hrs.toFixed(1)}h remaining`
|
|
275
|
+
: `expired ${Math.abs(hrs).toFixed(1)}h ago`;
|
|
276
|
+
const problems = (a.problems || []).length
|
|
277
|
+
? '<ul style="margin:8px 0 0 16px; font-size:11px; color:var(--red)">' +
|
|
278
|
+
a.problems.map((x) => `<li>${escapeHtml(x)}</li>`).join('') +
|
|
279
|
+
'</ul>'
|
|
280
|
+
: '';
|
|
281
|
+
return `
|
|
227
282
|
<div class="card approval-card">
|
|
228
283
|
<div class="card-body">
|
|
229
284
|
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:12px;">
|
|
@@ -241,33 +296,64 @@ function renderApprovals() {
|
|
|
241
296
|
${problems}
|
|
242
297
|
</div>
|
|
243
298
|
</div>`;
|
|
244
|
-
|
|
299
|
+
})
|
|
300
|
+
.join('');
|
|
245
301
|
}
|
|
246
302
|
|
|
247
303
|
function renderTeam(data) {
|
|
304
|
+
// Metrics.getTeamActivity() returns { active, conflicts } (not a flat array), and each
|
|
305
|
+
// `active` entry has email/last_seen/current_task/event fields, not user/action/timestamp.
|
|
306
|
+
// Reading `data.length`/`data.map()` on that object never rendered anything: `.length` is
|
|
307
|
+
// undefined so the empty-state guard below never fired, and `.map()` on a plain object threw,
|
|
308
|
+
// silently swallowed by refreshTeam()'s catch.
|
|
248
309
|
const list = document.getElementById('team-activity-list');
|
|
249
|
-
|
|
250
|
-
|
|
310
|
+
const active = Array.isArray(data?.active) ? data.active : [];
|
|
311
|
+
if (active.length === 0) {
|
|
312
|
+
list.innerHTML =
|
|
313
|
+
'<div style="text-align:center; padding: 40px; color:var(--muted)">No recent team activity</div>';
|
|
251
314
|
return;
|
|
252
315
|
}
|
|
253
|
-
list.innerHTML =
|
|
316
|
+
list.innerHTML = active
|
|
317
|
+
.map(
|
|
318
|
+
(ev) => `
|
|
254
319
|
<div class="activity-row">
|
|
255
|
-
<div class="avatar">${(ev.
|
|
320
|
+
<div class="avatar">${(ev.email || '?').charAt(0).toUpperCase()}</div>
|
|
256
321
|
<div class="activity-info">
|
|
257
|
-
<div><span class="activity-user">${escapeHtml(ev.
|
|
258
|
-
<div class="activity-time">${new Date(ev.
|
|
322
|
+
<div><span class="activity-user">${escapeHtml(ev.email)}</span> <span class="activity-action">${escapeHtml(ev.current_task || ev.event || '')}</span></div>
|
|
323
|
+
<div class="activity-time">${new Date(ev.last_seen).toLocaleString()}</div>
|
|
259
324
|
</div>
|
|
260
325
|
</div>
|
|
261
|
-
|
|
326
|
+
`,
|
|
327
|
+
)
|
|
328
|
+
.join('');
|
|
262
329
|
}
|
|
263
330
|
|
|
264
331
|
// ── Simple Charts Implementation ──────────────────────────────────────────
|
|
265
332
|
function drawCharts() {
|
|
266
|
-
|
|
267
|
-
|
|
333
|
+
// refreshData() does `state = data` with the raw /api/metrics response, which is
|
|
334
|
+
// { sessions, avg_quality, avg_cost_usd, ... } — it has never had `costs`/`quality`
|
|
335
|
+
// properties, so state.costs/state.quality were always undefined past the first load
|
|
336
|
+
// and these charts rendered permanently empty. The real per-session series lives at
|
|
337
|
+
// state.sessions[].cost_usd / state.sessions[].quality_score.
|
|
338
|
+
const sessions = state.sessions || [];
|
|
339
|
+
drawCanvasChart(
|
|
340
|
+
'chart-costs',
|
|
341
|
+
sessions.map((s) => s.cost_usd),
|
|
342
|
+
varColor('--accent'),
|
|
343
|
+
);
|
|
344
|
+
drawCanvasChart(
|
|
345
|
+
'chart-quality',
|
|
346
|
+
sessions.map((s) => s.quality_score),
|
|
347
|
+
varColor('--green'),
|
|
348
|
+
100,
|
|
349
|
+
);
|
|
268
350
|
}
|
|
269
351
|
|
|
270
|
-
function varColor(name) {
|
|
352
|
+
function varColor(name) {
|
|
353
|
+
return getComputedStyle(document.documentElement)
|
|
354
|
+
.getPropertyValue(name)
|
|
355
|
+
.trim();
|
|
356
|
+
}
|
|
271
357
|
|
|
272
358
|
function drawCanvasChart(id, data, color, maxVal = null) {
|
|
273
359
|
const canvas = document.getElementById(id);
|
|
@@ -280,27 +366,30 @@ function drawCanvasChart(id, data, color, maxVal = null) {
|
|
|
280
366
|
|
|
281
367
|
const w = canvas.offsetWidth;
|
|
282
368
|
const h = canvas.offsetHeight;
|
|
283
|
-
ctx.clearRect(0,0,w,h);
|
|
284
|
-
|
|
369
|
+
ctx.clearRect(0, 0, w, h);
|
|
370
|
+
|
|
285
371
|
if (data.length < 2) return;
|
|
286
|
-
|
|
372
|
+
|
|
287
373
|
const max = maxVal || Math.max(...data) * 1.2 || 1;
|
|
288
374
|
const step = w / (data.length - 1);
|
|
289
|
-
|
|
375
|
+
|
|
290
376
|
ctx.beginPath();
|
|
291
377
|
ctx.strokeStyle = color;
|
|
292
378
|
ctx.lineWidth = 2;
|
|
293
379
|
ctx.lineJoin = 'round';
|
|
294
|
-
|
|
380
|
+
|
|
295
381
|
data.forEach((val, i) => {
|
|
296
382
|
const x = i * step;
|
|
297
383
|
const y = h - (val / max) * h;
|
|
298
|
-
if (i === 0) ctx.moveTo(x, y);
|
|
384
|
+
if (i === 0) ctx.moveTo(x, y);
|
|
385
|
+
else ctx.lineTo(x, y);
|
|
299
386
|
});
|
|
300
387
|
ctx.stroke();
|
|
301
388
|
|
|
302
389
|
// Area fill
|
|
303
|
-
ctx.lineTo(w, h);
|
|
390
|
+
ctx.lineTo(w, h);
|
|
391
|
+
ctx.lineTo(0, h);
|
|
392
|
+
ctx.closePath();
|
|
304
393
|
ctx.fillStyle = color.replace(')', ', 0.1)').replace('rgb', 'rgba');
|
|
305
394
|
ctx.fill();
|
|
306
395
|
}
|
|
@@ -309,7 +398,7 @@ function drawCanvasChart(id, data, color, maxVal = null) {
|
|
|
309
398
|
function renderTemporalTimeline() {
|
|
310
399
|
const slider = document.getElementById('temporal-slider');
|
|
311
400
|
const count = document.getElementById('timeline-count');
|
|
312
|
-
|
|
401
|
+
|
|
313
402
|
if (temporalHistory.length === 0) {
|
|
314
403
|
slider.max = 0;
|
|
315
404
|
count.textContent = '0 Snapshots';
|
|
@@ -318,11 +407,11 @@ function renderTemporalTimeline() {
|
|
|
318
407
|
|
|
319
408
|
slider.max = temporalHistory.length - 1;
|
|
320
409
|
count.textContent = `${temporalHistory.length} Snapshots`;
|
|
321
|
-
|
|
410
|
+
|
|
322
411
|
// Default to latest if not touching
|
|
323
412
|
if (slider.value == 0 && temporalHistory.length > 0) {
|
|
324
|
-
|
|
325
|
-
|
|
413
|
+
onSliderChange(temporalHistory.length - 1);
|
|
414
|
+
slider.value = temporalHistory.length - 1;
|
|
326
415
|
}
|
|
327
416
|
}
|
|
328
417
|
|
|
@@ -332,16 +421,19 @@ async function onSliderChange(index) {
|
|
|
332
421
|
if (!snap) return;
|
|
333
422
|
|
|
334
423
|
selectedSnapshot = snap;
|
|
335
|
-
document.getElementById('slider-current').textContent =
|
|
424
|
+
document.getElementById('slider-current').textContent =
|
|
425
|
+
`Point: ${snap.id.slice(0, 8)} (${new Date(snap.timestamp).toLocaleTimeString()})`;
|
|
336
426
|
document.getElementById('inject-btn').disabled = false;
|
|
337
427
|
|
|
338
428
|
// Fetch sample audit file for this snapshot
|
|
339
429
|
try {
|
|
340
430
|
const res = await fetch(`/api/temporal/snapshot/${snap.id}/AUDIT.jsonl`);
|
|
341
431
|
const content = await res.text();
|
|
342
|
-
document.getElementById('snapshot-viewer').textContent =
|
|
343
|
-
|
|
344
|
-
|
|
432
|
+
document.getElementById('snapshot-viewer').textContent =
|
|
433
|
+
content || 'No audit log available for this point.';
|
|
434
|
+
} catch (e) {
|
|
435
|
+
document.getElementById('snapshot-viewer').textContent =
|
|
436
|
+
'Failed to load snapshot details.';
|
|
345
437
|
}
|
|
346
438
|
}
|
|
347
439
|
|
|
@@ -349,8 +441,8 @@ async function injectHindsight() {
|
|
|
349
441
|
if (!selectedSnapshot) return;
|
|
350
442
|
const instruction = document.getElementById('steering-input').value;
|
|
351
443
|
if (!instruction) {
|
|
352
|
-
|
|
353
|
-
|
|
444
|
+
alert('Please provide a steering instruction for the hindsight injection.');
|
|
445
|
+
return;
|
|
354
446
|
}
|
|
355
447
|
|
|
356
448
|
const btn = document.getElementById('inject-btn');
|
|
@@ -363,50 +455,59 @@ async function injectHindsight() {
|
|
|
363
455
|
headers: { 'Content-Type': 'application/json' },
|
|
364
456
|
body: JSON.stringify({
|
|
365
457
|
auditId: selectedSnapshot.id,
|
|
366
|
-
fixDescription: instruction
|
|
367
|
-
})
|
|
458
|
+
fixDescription: instruction,
|
|
459
|
+
}),
|
|
368
460
|
});
|
|
369
461
|
const result = await res.json();
|
|
370
462
|
if (result.success) {
|
|
371
|
-
|
|
372
|
-
|
|
463
|
+
alert(
|
|
464
|
+
'Hindsight Injection Successful. Agent state rolled back and awaiting re-optimization.',
|
|
465
|
+
);
|
|
466
|
+
showPage('activity');
|
|
373
467
|
} else {
|
|
374
|
-
|
|
468
|
+
alert('Injection failed: ' + result.error);
|
|
375
469
|
}
|
|
376
|
-
} catch(e) {
|
|
377
|
-
|
|
470
|
+
} catch (e) {
|
|
471
|
+
alert('Failed to connect to temporal engine.');
|
|
378
472
|
} finally {
|
|
379
|
-
|
|
380
|
-
|
|
473
|
+
btn.disabled = false;
|
|
474
|
+
btn.textContent = 'Rewind & Inject Fix';
|
|
381
475
|
}
|
|
382
476
|
}
|
|
383
477
|
|
|
384
478
|
// ── RevOps Logic ─────────────────────────────────────────────────────────
|
|
385
479
|
function updateRevOpsUI(data) {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
480
|
+
const { roi, velocity, debt } = data;
|
|
481
|
+
|
|
482
|
+
document.getElementById('rev-roi').textContent = `${roi.roi_percentage}%`;
|
|
483
|
+
document.getElementById('rev-net').textContent = `$${roi.net_value}`;
|
|
484
|
+
document.getElementById('rev-velocity').innerHTML =
|
|
485
|
+
`${velocity.avg_seconds_per_task} <span class="stat-unit">sec/task</span>`;
|
|
486
|
+
document.getElementById('rev-eta').textContent = `ETA: ${velocity.eta}`;
|
|
487
|
+
document.getElementById('rev-health').textContent =
|
|
488
|
+
debt.security_health_score;
|
|
489
|
+
|
|
490
|
+
const healthStatus = document.getElementById('rev-health-status');
|
|
491
|
+
healthStatus.textContent = `STATUS: ${debt.governance_status.toUpperCase()}`;
|
|
492
|
+
healthStatus.style.background =
|
|
493
|
+
debt.security_health_score > 80
|
|
494
|
+
? 'rgba(63, 185, 80, 0.15)'
|
|
495
|
+
: 'rgba(248, 81, 73, 0.15)';
|
|
496
|
+
healthStatus.style.color =
|
|
497
|
+
debt.security_health_score > 80 ? 'var(--green)' : 'var(--red)';
|
|
498
|
+
|
|
499
|
+
document.getElementById('roi-hours').textContent = `${roi.hours_saved}h`;
|
|
500
|
+
document.getElementById('roi-gross').textContent = `$${roi.gross_value}`;
|
|
501
|
+
document.getElementById('roi-burn').textContent = `$${roi.token_cost}`;
|
|
502
|
+
document.getElementById('roi-total-pct').textContent =
|
|
503
|
+
`${roi.roi_percentage}%`;
|
|
504
|
+
|
|
505
|
+
document.getElementById('debt-critical').textContent = debt.critical_findings;
|
|
506
|
+
document.getElementById('debt-tier3').textContent = debt.tier3_approvals;
|
|
507
|
+
const riskBadge = document.getElementById('debt-risk');
|
|
508
|
+
riskBadge.textContent = debt.debt_level.toUpperCase();
|
|
509
|
+
riskBadge.className = `badge ${debt.debt_level === 'Minimal' ? 'badge-live' : ''}`;
|
|
510
|
+
if (debt.debt_level !== 'Minimal') riskBadge.style.color = 'var(--yellow)';
|
|
410
511
|
}
|
|
411
512
|
|
|
412
513
|
window.onresize = drawCharts;
|
|
@@ -421,9 +522,13 @@ document.querySelectorAll('nav .tab[data-page]').forEach((btn) => {
|
|
|
421
522
|
btn.addEventListener('click', () => showPage(btn.dataset.page));
|
|
422
523
|
});
|
|
423
524
|
const __slider = document.getElementById('temporal-slider');
|
|
424
|
-
if (__slider)
|
|
525
|
+
if (__slider)
|
|
526
|
+
__slider.addEventListener('input', (e) => onSliderChange(e.target.value));
|
|
425
527
|
document.querySelectorAll('[data-action]').forEach((el) => {
|
|
426
|
-
const fns = {
|
|
528
|
+
const fns = {
|
|
529
|
+
'inject-hindsight': () => injectHindsight(),
|
|
530
|
+
'close-confirm': () => closeConfirm(),
|
|
531
|
+
};
|
|
427
532
|
const fn = fns[el.dataset.action];
|
|
428
533
|
if (fn) el.addEventListener('click', fn);
|
|
429
534
|
});
|
|
@@ -5,49 +5,61 @@
|
|
|
5
5
|
*/
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
|
-
const Loader
|
|
8
|
+
const Loader = require('./source-loader');
|
|
9
9
|
const Generator = require('./skill-generator');
|
|
10
|
-
const Scorer
|
|
10
|
+
const Scorer = require('./skill-scorer');
|
|
11
11
|
const Registrar = require('./skill-registrar');
|
|
12
12
|
|
|
13
13
|
const ARGS = process.argv.slice(2);
|
|
14
14
|
const SOURCE = ARGS[0];
|
|
15
|
-
const NAME
|
|
15
|
+
const NAME = ARGS[1] || 'new-skill';
|
|
16
16
|
|
|
17
17
|
if (!SOURCE) {
|
|
18
|
-
console.log(
|
|
18
|
+
console.log(
|
|
19
|
+
'Usage: node bin/skills-builder/learn-cli.js <source-url-or-path> [skill-name]',
|
|
20
|
+
);
|
|
19
21
|
process.exit(1);
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
async function main() {
|
|
23
25
|
try {
|
|
24
26
|
console.log(`\n📚 Learning skill: ${NAME} from ${SOURCE}`);
|
|
25
|
-
|
|
27
|
+
|
|
26
28
|
// 1. Load
|
|
27
|
-
const { content, metadata } = await Loader.load(SOURCE);
|
|
28
|
-
|
|
29
|
+
const { type, content, metadata } = await Loader.load(SOURCE);
|
|
30
|
+
|
|
29
31
|
// 2. Generate
|
|
30
32
|
const result = await Generator.generate({
|
|
31
33
|
skillName: NAME,
|
|
32
34
|
content,
|
|
33
35
|
sourceMetadata: metadata,
|
|
34
|
-
sessionId: 'cli-learn'
|
|
36
|
+
sessionId: 'cli-learn',
|
|
35
37
|
});
|
|
36
|
-
|
|
38
|
+
|
|
37
39
|
// 3. Score
|
|
38
40
|
const scoreResult = Scorer.score(result.skillPath);
|
|
39
41
|
console.log(`\n✅ Skill generated: ${result.skillPath}`);
|
|
40
|
-
console.log(
|
|
41
|
-
|
|
42
|
+
console.log(
|
|
43
|
+
`⭐ Quality Score: ${scoreResult.quality_score}/100 (${scoreResult.threshold_status})`,
|
|
44
|
+
);
|
|
45
|
+
|
|
42
46
|
// 4. Register if acceptable
|
|
43
47
|
if (scoreResult.can_register) {
|
|
44
|
-
Registrar.register(
|
|
48
|
+
Registrar.register({
|
|
49
|
+
skillName: NAME,
|
|
50
|
+
skillPath: result.skillPath,
|
|
51
|
+
tier: 'project',
|
|
52
|
+
qualityScore: scoreResult.quality_score,
|
|
53
|
+
sourceType: type,
|
|
54
|
+
source: SOURCE,
|
|
55
|
+
});
|
|
45
56
|
console.log('📝 Registered in MANIFEST.md');
|
|
46
57
|
} else {
|
|
47
58
|
console.log('⚠️ Score too low for automatic registration.');
|
|
48
|
-
scoreResult.improvement_suggestions.forEach(s =>
|
|
59
|
+
scoreResult.improvement_suggestions.forEach((s) =>
|
|
60
|
+
console.log(` - ${s}`),
|
|
61
|
+
);
|
|
49
62
|
}
|
|
50
|
-
|
|
51
63
|
} catch (err) {
|
|
52
64
|
console.error(`\n❌ Error: ${err.message}`);
|
|
53
65
|
process.exit(1);
|
|
@@ -100,12 +100,28 @@ function releaseReadinessChecks(root) {
|
|
|
100
100
|
pass: Boolean(pkgVersion) && pkgVersion === configVersion,
|
|
101
101
|
fix: `Align versions: package.json=${pkgVersion || '?'} vs config.json=${configVersion || '?'}`,
|
|
102
102
|
},
|
|
103
|
+
// CHECKS CONTENT, NOT MERE EXISTENCE, and that change has a cost worth naming: from now on every
|
|
104
|
+
// release needs a RELEASENOTES entry. Six published 11.x versions do not have one (11.0.0, 11.2.0,
|
|
105
|
+
// 11.2.1, 11.4.0, 11.7.1, 11.9.1) — the file has been curated rather than exhaustive. Those gaps are
|
|
106
|
+
// unaffected, because this only ever looks for the version being released.
|
|
107
|
+
//
|
|
108
|
+
// WHY IT CHANGED. This gate passed on `fileExists` while its changelog sibling three lines down
|
|
109
|
+
// checked that the version was IN the file. That asymmetry is why both prose surfaces shipped stale
|
|
110
|
+
// twice running: the immutable 11.9.3 tarball's README said "Latest release v11.9.2", and the
|
|
111
|
+
// immutable 11.9.4 tarball shipped with no 11.9.4 entry here at all — while README.md offers this
|
|
112
|
+
// file as the human-readable route to the BREAKING notes. 348a3a2c fixed the identical defect one
|
|
113
|
+
// release earlier by hand and added no gate, and it recurred immediately.
|
|
114
|
+
//
|
|
115
|
+
// Anchored, not `includes`. A substring test for "11.9.4" is also satisfied by "## v11.9.40" and by
|
|
116
|
+
// "## v11.9.4-notyet" — measured, when the equivalent test assertion was falsified.
|
|
103
117
|
{
|
|
104
118
|
id: 'releasenotes',
|
|
105
|
-
label: 'RELEASENOTES.md present',
|
|
119
|
+
label: 'RELEASENOTES.md present and has an entry for the current version',
|
|
106
120
|
points: 2,
|
|
107
|
-
pass: fileExists(root, 'RELEASENOTES.md')
|
|
108
|
-
|
|
121
|
+
pass: fileExists(root, 'RELEASENOTES.md') && Boolean(pkgVersion)
|
|
122
|
+
&& new RegExp(`^## v${pkgVersion.replace(/\./g, '\\.')}(?![\\d.\\w-])`, 'm')
|
|
123
|
+
.test(safeRead(root, 'RELEASENOTES.md')),
|
|
124
|
+
fix: `Add a "## v${pkgVersion || '?'}" section to RELEASENOTES.md. README.md links here as the human-readable route to the BREAKING notes, so releasing without one points readers at a file whose newest entry describes a different version.`,
|
|
109
125
|
},
|
|
110
126
|
{
|
|
111
127
|
id: 'changelog',
|