reflectt-node 0.1.7 → 0.1.8
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 +13 -0
- package/defaults/gitignore.template +23 -0
- package/dist/boardHealthWorker.d.ts +4 -0
- package/dist/boardHealthWorker.d.ts.map +1 -1
- package/dist/boardHealthWorker.js +36 -1
- package/dist/boardHealthWorker.js.map +1 -1
- package/dist/buildInfo.d.ts.map +1 -1
- package/dist/buildInfo.js +47 -10
- package/dist/buildInfo.js.map +1 -1
- package/dist/chat.d.ts +4 -0
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +6 -2
- package/dist/chat.js.map +1 -1
- package/dist/cli.js +37 -12
- package/dist/cli.js.map +1 -1
- package/dist/cloud.d.ts.map +1 -1
- package/dist/cloud.js +131 -64
- package/dist/cloud.js.map +1 -1
- package/dist/continuity-loop.d.ts.map +1 -1
- package/dist/continuity-loop.js +297 -29
- package/dist/continuity-loop.js.map +1 -1
- package/dist/deploy-monitor.d.ts +18 -0
- package/dist/deploy-monitor.d.ts.map +1 -0
- package/dist/deploy-monitor.js +165 -0
- package/dist/deploy-monitor.js.map +1 -0
- package/dist/executionSweeper.d.ts +1 -0
- package/dist/executionSweeper.d.ts.map +1 -1
- package/dist/executionSweeper.js +43 -7
- package/dist/executionSweeper.js.map +1 -1
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +17 -3
- package/dist/files.js.map +1 -1
- package/dist/fingerprint.d.ts +30 -0
- package/dist/fingerprint.d.ts.map +1 -0
- package/dist/fingerprint.js +122 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/github-webhook-attribution.d.ts +38 -0
- package/dist/github-webhook-attribution.d.ts.map +1 -0
- package/dist/github-webhook-attribution.js +123 -0
- package/dist/github-webhook-attribution.js.map +1 -0
- package/dist/inbox.d.ts.map +1 -1
- package/dist/inbox.js +4 -0
- package/dist/inbox.js.map +1 -1
- package/dist/index.js +37 -1
- package/dist/index.js.map +1 -1
- package/dist/pulse.d.ts +7 -0
- package/dist/pulse.d.ts.map +1 -1
- package/dist/pulse.js +15 -0
- package/dist/pulse.js.map +1 -1
- package/dist/review-state.d.ts +9 -0
- package/dist/review-state.d.ts.map +1 -0
- package/dist/review-state.js +17 -0
- package/dist/review-state.js.map +1 -0
- package/dist/schedule.d.ts +60 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +176 -0
- package/dist/schedule.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +486 -14
- package/dist/server.js.map +1 -1
- package/dist/suppression-ledger.d.ts.map +1 -1
- package/dist/suppression-ledger.js +12 -3
- package/dist/suppression-ledger.js.map +1 -1
- package/dist/system-loop-state.d.ts +1 -1
- package/dist/system-loop-state.d.ts.map +1 -1
- package/dist/system-loop-state.js +1 -0
- package/dist/system-loop-state.js.map +1 -1
- package/dist/tasks.d.ts +9 -1
- package/dist/tasks.d.ts.map +1 -1
- package/dist/tasks.js +193 -41
- package/dist/tasks.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/usage-tracking.d.ts +26 -0
- package/dist/usage-tracking.d.ts.map +1 -1
- package/dist/usage-tracking.js +91 -4
- package/dist/usage-tracking.js.map +1 -1
- package/package.json +1 -1
- package/public/dashboard.js +119 -37
- package/public/docs.md +18 -0
package/public/dashboard.js
CHANGED
|
@@ -726,9 +726,10 @@ async function loadPresence() {
|
|
|
726
726
|
// ---- Tasks ----
|
|
727
727
|
async function loadTasks(forceFull = false) {
|
|
728
728
|
try {
|
|
729
|
-
|
|
729
|
+
// Force full load if we have no tasks yet (e.g. first load failed)
|
|
730
|
+
const useDelta = !forceFull && lastTaskSync > 0 && allTasks.length > 0;
|
|
730
731
|
const qs = new URLSearchParams();
|
|
731
|
-
qs.set('limit', '
|
|
732
|
+
qs.set('limit', '200');
|
|
732
733
|
if (useDelta) qs.set('updatedSince', String(lastTaskSync));
|
|
733
734
|
|
|
734
735
|
const r = await fetch(BASE + '/tasks?' + qs.toString());
|
|
@@ -1384,16 +1385,8 @@ async function loadSharedArtifacts() {
|
|
|
1384
1385
|
const pinnedRows = pinned.map(p => {
|
|
1385
1386
|
const found = files.find(f => (String(f.name || '')).toLowerCase() === p.name.toLowerCase());
|
|
1386
1387
|
if (!found) {
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
<div style="font-size:13px;color:var(--text-bright);font-weight:600">${esc(p.label)}</div>
|
|
1390
|
-
<span class="assignee-tag" style="color:var(--yellow)">missing</span>
|
|
1391
|
-
</div>
|
|
1392
|
-
<div style="font-size:11px;color:var(--text-muted);margin-top:4px">
|
|
1393
|
-
To make this available here, create a symlink in the shared workspace:<br/>
|
|
1394
|
-
<code>ln -s ../RYANS-THOUGHTS.md ~/.openclaw/workspace-shared/process/RYANS-THOUGHTS.md</code>
|
|
1395
|
-
</div>
|
|
1396
|
-
</div>`;
|
|
1388
|
+
// Operator notes are optional. If not configured, do not show an operator-only instruction to end users.
|
|
1389
|
+
return '';
|
|
1397
1390
|
}
|
|
1398
1391
|
const href = '/shared/view?path=' + encodeURIComponent(found.path);
|
|
1399
1392
|
return `<div style="padding:10px 12px;border-bottom:1px solid var(--border-subtle)">
|
|
@@ -1422,9 +1415,13 @@ async function loadSharedArtifacts() {
|
|
|
1422
1415
|
</div>`;
|
|
1423
1416
|
}).join('');
|
|
1424
1417
|
|
|
1418
|
+
const pinnedBox = pinnedRows
|
|
1419
|
+
? `<div style="border:1px solid var(--border-subtle);border-radius:8px;overflow:hidden">${pinnedRows}</div>
|
|
1420
|
+
<div style="height:10px"></div>`
|
|
1421
|
+
: '';
|
|
1422
|
+
|
|
1425
1423
|
body.innerHTML = `
|
|
1426
|
-
|
|
1427
|
-
<div style="height:10px"></div>
|
|
1424
|
+
${pinnedBox}
|
|
1428
1425
|
<div style="font-size:11px;color:var(--text-muted);margin:0 0 8px">Shared workspace directory: <code>process/</code></div>
|
|
1429
1426
|
<div style="border:1px solid var(--border-subtle);border-radius:8px;overflow:hidden">${listRows}</div>
|
|
1430
1427
|
`;
|
|
@@ -1806,6 +1803,18 @@ function normalizeEpochMs(v) {
|
|
|
1806
1803
|
return v;
|
|
1807
1804
|
}
|
|
1808
1805
|
|
|
1806
|
+
function hasReviewerDecision(task) {
|
|
1807
|
+
const meta = task && task.metadata && typeof task.metadata === 'object' ? task.metadata : null;
|
|
1808
|
+
const decision = meta && meta.reviewer_decision && typeof meta.reviewer_decision === 'object'
|
|
1809
|
+
? meta.reviewer_decision
|
|
1810
|
+
: null;
|
|
1811
|
+
return !!decision;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
function shouldEscalateReviewerSla(task) {
|
|
1815
|
+
return task && task.slaState === 'breach' && !hasReviewerDecision(task);
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1809
1818
|
function renderReviewQueue() {
|
|
1810
1819
|
const panel = document.getElementById('review-queue-panel');
|
|
1811
1820
|
const body = document.getElementById('review-queue-body');
|
|
@@ -1817,11 +1826,23 @@ function renderReviewQueue() {
|
|
|
1817
1826
|
const validating = allTasks
|
|
1818
1827
|
.filter(t => t.status === 'validating')
|
|
1819
1828
|
.map(t => {
|
|
1820
|
-
const
|
|
1829
|
+
const meta = t.metadata || {};
|
|
1830
|
+
const reviewState = typeof meta.review_state === 'string' ? meta.review_state : '';
|
|
1831
|
+
const reviewerDecision = meta.reviewer_decision;
|
|
1832
|
+
|
|
1833
|
+
// If reviewer has acted (needs_author or reviewer_decision recorded), the ball is with the assignee.
|
|
1834
|
+
// We still track an SLA timer, but it should page the assignee (author), not the reviewer.
|
|
1835
|
+
const waitOn = (reviewState === 'needs_author' || reviewerDecision != null) ? 'author' : 'reviewer';
|
|
1836
|
+
|
|
1837
|
+
const rawEntered = waitOn === 'author'
|
|
1838
|
+
? (reviewerDecision && reviewerDecision.decidedAt) || meta.review_last_activity_at || meta.entered_validating_at || t.updatedAt || t.createdAt
|
|
1839
|
+
: meta.entered_validating_at || t.updatedAt || t.createdAt;
|
|
1840
|
+
|
|
1821
1841
|
const enteredAt = normalizeEpochMs(rawEntered) || now;
|
|
1822
1842
|
const timeInReview = Math.min(Math.max(0, now - enteredAt), MAX_REVIEW_MS);
|
|
1823
1843
|
const slaState = getReviewSlaState(timeInReview);
|
|
1824
|
-
|
|
1844
|
+
|
|
1845
|
+
return { ...t, timeInReview, slaState, enteredAt, waitOn, reviewState, hasReviewerDecision: reviewerDecision != null };
|
|
1825
1846
|
})
|
|
1826
1847
|
.sort((a, b) => {
|
|
1827
1848
|
// Breaches first, then by time descending
|
|
@@ -1831,6 +1852,9 @@ function renderReviewQueue() {
|
|
|
1831
1852
|
return b.timeInReview - a.timeInReview;
|
|
1832
1853
|
});
|
|
1833
1854
|
|
|
1855
|
+
const reviewerQueue = validating.filter(t => t.waitOn === 'reviewer');
|
|
1856
|
+
const authorQueue = validating.filter(t => t.waitOn === 'author');
|
|
1857
|
+
|
|
1834
1858
|
if (validating.length === 0) {
|
|
1835
1859
|
panel.style.display = '';
|
|
1836
1860
|
count.textContent = '';
|
|
@@ -1846,16 +1870,29 @@ function renderReviewQueue() {
|
|
|
1846
1870
|
}
|
|
1847
1871
|
|
|
1848
1872
|
panel.style.display = '';
|
|
1849
|
-
|
|
1850
|
-
|
|
1873
|
+
const reviewerBreachCount = reviewerQueue.filter(t => t.slaState === 'breach').length;
|
|
1874
|
+
const authorBreachCount = authorQueue.filter(t => t.slaState === 'breach').length;
|
|
1875
|
+
|
|
1876
|
+
// Primary count is "awaiting reviewer" — author-wait tasks are tracked separately.
|
|
1877
|
+
count.textContent = reviewerQueue.length + ' awaiting review';
|
|
1878
|
+
|
|
1879
|
+
// Update sidebar badge (reviewer queue only)
|
|
1851
1880
|
const navReviewBadge = document.getElementById('nav-review-count');
|
|
1852
|
-
if (navReviewBadge) navReviewBadge.textContent =
|
|
1881
|
+
if (navReviewBadge) navReviewBadge.textContent = String(reviewerQueue.length);
|
|
1882
|
+
|
|
1883
|
+
const headerExtra = (reviewerBreachCount > 0 || authorBreachCount > 0)
|
|
1884
|
+
? ' <span style="color:var(--red);font-size:11px;font-weight:600">'
|
|
1885
|
+
+ (reviewerBreachCount > 0 ? (reviewerBreachCount + ' reviewer breach' + (reviewerBreachCount > 1 ? 'es' : '')) : '')
|
|
1886
|
+
+ (reviewerBreachCount > 0 && authorBreachCount > 0 ? ' · ' : '')
|
|
1887
|
+
+ (authorBreachCount > 0 ? (authorBreachCount + ' author breach' + (authorBreachCount > 1 ? 'es' : '')) : '')
|
|
1888
|
+
+ '</span>'
|
|
1889
|
+
: '';
|
|
1853
1890
|
|
|
1854
|
-
const
|
|
1855
|
-
|
|
1856
|
-
? ' <span style="color:var(--red);font-size:11px;font-weight:600">' + breachCount + ' breach' + (breachCount > 1 ? 'es' : '') + '</span>'
|
|
1891
|
+
const authorExtra = authorQueue.length > 0
|
|
1892
|
+
? ' <span style="color:var(--text-muted);font-size:11px;font-weight:500">· ' + authorQueue.length + ' awaiting author</span>'
|
|
1857
1893
|
: '';
|
|
1858
|
-
|
|
1894
|
+
|
|
1895
|
+
count.innerHTML = reviewerQueue.length + ' awaiting review' + authorExtra + headerExtra;
|
|
1859
1896
|
|
|
1860
1897
|
body.innerHTML = validating.map(t => {
|
|
1861
1898
|
const reviewer = t.reviewer || '<span style="color:var(--yellow)">unassigned</span>';
|
|
@@ -1884,33 +1921,40 @@ function renderReviewQueue() {
|
|
|
1884
1921
|
|
|
1885
1922
|
bindTaskLinkHandlers(body);
|
|
1886
1923
|
|
|
1887
|
-
// SLA breach escalation:
|
|
1888
|
-
|
|
1889
|
-
|
|
1924
|
+
// SLA breach escalation: split reviewer-wait vs author-wait so we page the right person.
|
|
1925
|
+
// shouldEscalateReviewerSla() suppresses escalation when reviewer_decision already exists.
|
|
1926
|
+
if (reviewerBreachCount > 0) {
|
|
1927
|
+
escalateReviewerBreaches(reviewerQueue.filter(shouldEscalateReviewerSla));
|
|
1928
|
+
}
|
|
1929
|
+
if (authorBreachCount > 0) {
|
|
1930
|
+
escalateAuthorBreaches(authorQueue.filter(t => t.slaState === 'breach'));
|
|
1890
1931
|
}
|
|
1891
1932
|
}
|
|
1892
1933
|
|
|
1893
|
-
let
|
|
1934
|
+
let lastReviewerEscalationAt = 0;
|
|
1935
|
+
let lastAuthorEscalationAt = 0;
|
|
1894
1936
|
const REVIEW_ESCALATION_COOLDOWN = 20 * 60 * 1000; // 20m
|
|
1895
1937
|
|
|
1896
|
-
async function
|
|
1938
|
+
async function escalateReviewerBreaches(breachedTasks) {
|
|
1897
1939
|
const now = Date.now();
|
|
1898
|
-
if (now -
|
|
1899
|
-
|
|
1940
|
+
if (now - lastReviewerEscalationAt < REVIEW_ESCALATION_COOLDOWN) return;
|
|
1941
|
+
lastReviewerEscalationAt = now;
|
|
1900
1942
|
|
|
1901
1943
|
const lines = breachedTasks.slice(0, 5).map(t => {
|
|
1902
1944
|
const reviewer = t.reviewer || 'unassigned';
|
|
1903
|
-
|
|
1945
|
+
const prUrl = t.metadata?.review_handoff?.pr_url || t.metadata?.review_handoff?.prUrl || '';
|
|
1946
|
+
const prPart = prUrl ? ' — ' + prUrl : '';
|
|
1947
|
+
return '- ' + t.id + ' (' + (t.title || '').slice(0, 50) + ') — reviewer: @' + reviewer + ', waiting ' + formatDuration(t.timeInReview) + prPart;
|
|
1904
1948
|
});
|
|
1905
1949
|
|
|
1906
|
-
const content = '
|
|
1950
|
+
const content = 'Review overdue (reviewer SLA):\n' + lines.join('\n');
|
|
1907
1951
|
|
|
1908
1952
|
try {
|
|
1909
1953
|
await fetch(BASE + '/chat/messages', {
|
|
1910
1954
|
method: 'POST',
|
|
1911
1955
|
headers: { 'Content-Type': 'application/json' },
|
|
1912
1956
|
body: JSON.stringify({
|
|
1913
|
-
from: '
|
|
1957
|
+
from: 'dashboard',
|
|
1914
1958
|
content,
|
|
1915
1959
|
channel: 'general',
|
|
1916
1960
|
timestamp: now
|
|
@@ -1921,6 +1965,36 @@ async function escalateReviewBreaches(breachedTasks) {
|
|
|
1921
1965
|
}
|
|
1922
1966
|
}
|
|
1923
1967
|
|
|
1968
|
+
async function escalateAuthorBreaches(breachedTasks) {
|
|
1969
|
+
const now = Date.now();
|
|
1970
|
+
if (now - lastAuthorEscalationAt < REVIEW_ESCALATION_COOLDOWN) return;
|
|
1971
|
+
lastAuthorEscalationAt = now;
|
|
1972
|
+
|
|
1973
|
+
const lines = breachedTasks.slice(0, 5).map(t => {
|
|
1974
|
+
const assignee = t.assignee || 'unassigned';
|
|
1975
|
+
const prUrl = t.metadata?.review_handoff?.pr_url || t.metadata?.review_handoff?.prUrl || '';
|
|
1976
|
+
const prPart = prUrl ? ' — ' + prUrl : '';
|
|
1977
|
+
return '- ' + t.id + ' (' + (t.title || '').slice(0, 50) + ') — assignee: @' + assignee + ', waiting ' + formatDuration(t.timeInReview) + prPart;
|
|
1978
|
+
});
|
|
1979
|
+
|
|
1980
|
+
const content = 'Author action needed (post-review):\n' + lines.join('\n');
|
|
1981
|
+
|
|
1982
|
+
try {
|
|
1983
|
+
await fetch(BASE + '/chat/messages', {
|
|
1984
|
+
method: 'POST',
|
|
1985
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1986
|
+
body: JSON.stringify({
|
|
1987
|
+
from: 'dashboard',
|
|
1988
|
+
content,
|
|
1989
|
+
channel: 'general',
|
|
1990
|
+
timestamp: now
|
|
1991
|
+
})
|
|
1992
|
+
});
|
|
1993
|
+
} catch (err) {
|
|
1994
|
+
console.error('Failed to escalate author breach:', err);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1924
1998
|
// ---- Feedback ----
|
|
1925
1999
|
let feedbackData = null;
|
|
1926
2000
|
|
|
@@ -3020,13 +3094,21 @@ async function checkGettingStarted() {
|
|
|
3020
3094
|
|
|
3021
3095
|
function dismissGettingStarted() {
|
|
3022
3096
|
const panel = document.getElementById('getting-started');
|
|
3023
|
-
if (panel)
|
|
3097
|
+
if (panel) {
|
|
3098
|
+
panel.classList.add('hidden');
|
|
3099
|
+
panel.style.display = 'none'; // belt + suspenders
|
|
3100
|
+
}
|
|
3024
3101
|
try { localStorage.setItem('reflectt-gs-dismissed', '1'); } catch {}
|
|
3025
3102
|
}
|
|
3103
|
+
// Expose globally for onclick handlers (safety net)
|
|
3104
|
+
window.dismissGettingStarted = dismissGettingStarted;
|
|
3105
|
+
window.dismissFirstBootBanner = dismissFirstBootBanner;
|
|
3026
3106
|
|
|
3027
3107
|
updateClock();
|
|
3028
3108
|
setInterval(updateClock, 30000);
|
|
3029
3109
|
checkGettingStarted();
|
|
3110
|
+
// Retry getting-started check after 5s in case health wasn't ready at boot
|
|
3111
|
+
setTimeout(checkGettingStarted, 5000);
|
|
3030
3112
|
refresh();
|
|
3031
3113
|
connectEventStream();
|
|
3032
3114
|
startAdaptiveRefresh();
|
|
@@ -3406,7 +3488,7 @@ function fileIcon(mimeType, name) {
|
|
|
3406
3488
|
}
|
|
3407
3489
|
|
|
3408
3490
|
function formatFileSize(bytes) {
|
|
3409
|
-
if (
|
|
3491
|
+
if (typeof bytes !== 'number' || bytes <= 0) return '0 B';
|
|
3410
3492
|
if (bytes < 1024) return bytes + ' B';
|
|
3411
3493
|
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
|
3412
3494
|
return (bytes / 1048576).toFixed(1) + ' MB';
|
|
@@ -3455,7 +3537,7 @@ function renderFiles(files) {
|
|
|
3455
3537
|
return '<div class="file-card" tabindex="0" role="button" aria-label="' + (f.originalName || f.filename) + '">' +
|
|
3456
3538
|
'<div class="thumb" style="display:flex;align-items:center;justify-content:center;height:80px;background:var(--surface-raised);border-radius:var(--radius-sm);overflow:hidden">' + thumb + '</div>' +
|
|
3457
3539
|
'<div class="card-name">' + (f.originalName || f.filename) + '</div>' +
|
|
3458
|
-
'<div class="card-meta">' + formatFileSize(f.
|
|
3540
|
+
'<div class="card-meta">' + formatFileSize(f.sizeBytes) + ' · ' + timeAgo(f.uploadedAt || f.createdAt) + '</div>' +
|
|
3459
3541
|
'<div class="card-actions">' +
|
|
3460
3542
|
'<a href="/files/' + f.id + '" download="' + (f.originalName || f.filename) + '" class="action-btn" aria-label="Download" onclick="event.stopPropagation()">⬇</a>' +
|
|
3461
3543
|
'<button class="action-btn delete" aria-label="Delete" onclick="event.stopPropagation();deleteFile(\'' + f.id + '\')">🗑</button>' +
|
|
@@ -3468,7 +3550,7 @@ function renderFiles(files) {
|
|
|
3468
3550
|
return '<div class="file-list-item" tabindex="0" role="button">' +
|
|
3469
3551
|
'<span class="list-icon">' + icon + '</span>' +
|
|
3470
3552
|
'<span class="list-name">' + (f.originalName || f.filename) + '</span>' +
|
|
3471
|
-
'<span class="list-meta">' + formatFileSize(f.
|
|
3553
|
+
'<span class="list-meta">' + formatFileSize(f.sizeBytes) + '</span>' +
|
|
3472
3554
|
'<span class="list-meta">' + timeAgo(f.uploadedAt || f.createdAt) + '</span>' +
|
|
3473
3555
|
'<div class="list-actions">' +
|
|
3474
3556
|
'<a href="/files/' + f.id + '" download="' + (f.originalName || f.filename) + '" class="action-btn" aria-label="Download" onclick="event.stopPropagation()">⬇</a>' +
|
package/public/docs.md
CHANGED
|
@@ -216,7 +216,10 @@ If your deployment needs quiet-hours behavior today, enforce it in scheduler/gat
|
|
|
216
216
|
| POST | `/tasks/batch-create` | Batch create up to 20 tasks. Body: `{ "tasks": [...], "createdBy": "agent", "deduplicate": true, "dryRun": false }`. Each task follows the same schema as `POST /tasks`. Returns per-task results (created/duplicate/error) with summary counts. Deduplication checks exact title match + fuzzy word overlap (Jaccard >0.6) against active tasks. |
|
|
217
217
|
| GET | `/tasks/heartbeat-status` | All doing tasks with stale comment activity (>30m). Returns `{ threshold, doingTaskCount, staleCount, staleTasks[] }`. Use for monitoring status heartbeat discipline compliance. |
|
|
218
218
|
| GET | `/tasks/board-health` | Board-level health metrics for backlog replenishment. Returns per-agent breakdown (doing, validating, todo, active counts), `needsWork`/`lowWatermark` flags, and `replenishNeeded` trigger (fires when 2+ agents idle or <3 backlog tasks). Query: `include_test=1` to include test-harness tasks. |
|
|
219
|
+
| GET | `/agents` | Agent list — alias for /agents/roles. Returns all agents with roles, WIP status, affinity tags. |
|
|
219
220
|
| GET | `/agents/roles` | Agent role registry with live WIP status. Returns all agents with `name`, `displayName`, `role`, `affinityTags`, `protectedDomains`, `wipCap`, `wipCount`, `overCap`. |
|
|
221
|
+
| POST | `/agents` | Add agent to team. Body: `{ name, role, description?, affinityTags?, wipCap? }`. Hot-reloads TEAM-ROLES.yaml. |
|
|
222
|
+
| DELETE | `/agents/:name` | Remove agent from team. Hot-reloads TEAM-ROLES.yaml. |
|
|
220
223
|
| POST | `/config/identity` | Set an agent's display name. Body: `{ "agent": "agent-1", "displayName": "Juniper" }`. Persists to TEAM-ROLES.yaml, hot-reloads. Dashboard and mentions use display name. |
|
|
221
224
|
| PUT | `/config/team-roles` | Write TEAM-ROLES.yaml. Body: `{ "yaml": "agents:\n - name: link\n role: engineer..." }`. Hot-reloads on save. Used by bootstrap agent to configure team from user intent. |
|
|
222
225
|
| GET | `/resolve/mention/:mention` | Resolve a mention string (name, displayName, or alias) to canonical agent ID. Returns `{ agent, displayName, role }`. |
|
|
@@ -863,6 +866,7 @@ Set via `reflectionNudge` in policy config:
|
|
|
863
866
|
| POST | `/usage/caps` | Create spend cap. Body: `{ scope: "global"\|"agent"\|"team", scope_id?, period: "daily"\|"weekly"\|"monthly", limit_usd, action: "warn"\|"throttle"\|"block" }`. |
|
|
864
867
|
| DELETE | `/usage/caps/:id` | Delete a spend cap. |
|
|
865
868
|
| GET | `/usage/routing-suggestions` | Smart routing savings suggestions (which low-stakes categories could use cheaper models). Query: `since`. |
|
|
869
|
+
| GET | `/costs` | Cost dashboard — aggregated spend for COO/PM monitoring. Query: `days` (1–90, default 7). Returns: `daily_by_model` (spend per model per day), `daily_totals` (per-day rolled-up for threshold alerting), `avg_cost_by_lane` (avg cost per closed task by `qa_bundle.lane`, 30-day floor), `avg_cost_by_agent` (avg cost per closed task per agent + `top_model`, 30-day floor), `top_tasks_by_cost` (top 20 most expensive tasks in window), `summary` (total tokens + cost), `lane_agent_window_days` (actual window used for lane/agent averages). |
|
|
866
870
|
|
|
867
871
|
### Model Pricing (built-in estimates, per 1M tokens)
|
|
868
872
|
| Model | Input | Output |
|
|
@@ -1034,6 +1038,20 @@ Export → import preserves: summary, description, organizer, attendees, locatio
|
|
|
1034
1038
|
| GET | `/calendar/events/:id/export.ics` | Export single event as .ics file. |
|
|
1035
1039
|
| POST | `/calendar/import` | Import events from .ics content. Body: `{ ics: string, organizer?: string }` or raw .ics string. Returns created/updated events. UID-based dedup on re-import. |
|
|
1036
1040
|
|
|
1041
|
+
## Schedule Feed
|
|
1042
|
+
|
|
1043
|
+
Shared time-awareness for the team. Canonical records for deploy windows, focus blocks, and scheduled task work — so agents can coordinate timing without chat.
|
|
1044
|
+
|
|
1045
|
+
**MVP scope (v1):** One-off windows only. No iCal/RRULE, no reminders, no recurring rules. For per-agent availability and recurring blocks use `/calendar/blocks`. For notifications use the Calendar Reminder Engine.
|
|
1046
|
+
|
|
1047
|
+
| Method | Path | Description |
|
|
1048
|
+
|--------|------|-------------|
|
|
1049
|
+
| GET | `/schedule/feed` | Upcoming entries in chronological order. Query: `kinds` (comma-separated: `deploy_window,focus_block,scheduled_task`), `owner`, `after` (epoch ms, default: now), `before` (epoch ms), `limit` (default: 50, max: 200). |
|
|
1050
|
+
| POST | `/schedule/entries` | Create a schedule entry. Body: `{ kind, title, start, end, owner, task_id?, status?, meta? }`. `kind` must be `deploy_window`, `focus_block`, or `scheduled_task`. `start`/`end` are epoch ms. Default status: `open` / `active` / `pending`. |
|
|
1051
|
+
| GET | `/schedule/entries/:id` | Get a single entry by ID. |
|
|
1052
|
+
| PATCH | `/schedule/entries/:id` | Update an entry. Body: `{ title?, start?, end?, status?, meta? }`. |
|
|
1053
|
+
| DELETE | `/schedule/entries/:id` | Delete an entry. Returns 204. |
|
|
1054
|
+
|
|
1037
1055
|
## Remote Node Management
|
|
1038
1056
|
|
|
1039
1057
|
Auth-gated endpoints for managing a reflectt-node instance remotely. Provide `REFLECTT_MANAGE_TOKEN` env var; authenticate via `x-manage-token` header or `Authorization: Bearer <token>`. Loopback (localhost) access is always allowed.
|