mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
updated with files
This commit is contained in:
1 parent
70110adcc5
commit
67c8a17a03
14 files changed
+1793
No files matched your search
@@ -0,0 +1,61 @@
|
||||
# TORCH (Standalone)
|
||||
|
||||
TORCH is a portable Nostr-based task locking toolkit for multi-agent coordination.
|
||||
|
||||
## First-run quickstart
|
||||
|
||||
From the repository root:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run lock:check:daily
|
||||
AGENT_PLATFORM=codex npm run lock:lock -- --agent docs-agent --cadence daily
|
||||
npm run lock:list
|
||||
```
|
||||
|
||||
Optional dashboard:
|
||||
|
||||
```bash
|
||||
npm run dashboard:serve
|
||||
# then open http://localhost:4173/dashboard/
|
||||
```
|
||||
|
||||
## Included
|
||||
|
||||
- `src/nostr-lock.mjs` — Generic lock/check/list CLI
|
||||
- `src/docs/TORCH.md` — Protocol summary and usage
|
||||
- `src/prompts/` — Generic scheduler prompts and flow
|
||||
- `examples/bitvid/` — Bitvid-style scheduler overlay examples adapted for standalone TORCH paths
|
||||
- `dashboard/index.html` — Static lock dashboard
|
||||
|
||||
## CLI dependencies
|
||||
|
||||
Declared in `package.json` and pinned:
|
||||
|
||||
- `[email protected]`
|
||||
- `[email protected]`
|
||||
|
||||
## NPM script helpers
|
||||
|
||||
- `npm run lock:check:daily`
|
||||
- `npm run lock:check:weekly`
|
||||
- `npm run lock:list`
|
||||
- `npm run lock:lock -- --agent <agent-name> --cadence <daily|weekly>`
|
||||
- `npm run dashboard:serve`
|
||||
|
||||
## Environment variables
|
||||
|
||||
- `NOSTR_LOCK_NAMESPACE`
|
||||
- `NOSTR_LOCK_RELAYS`
|
||||
- `NOSTR_LOCK_TTL`
|
||||
- `NOSTR_LOCK_DAILY_ROSTER`
|
||||
- `NOSTR_LOCK_WEEKLY_ROSTER`
|
||||
- `AGENT_PLATFORM`
|
||||
|
||||
## Example
|
||||
|
||||
```bash
|
||||
NOSTR_LOCK_NAMESPACE=my-project \
|
||||
AGENT_PLATFORM=codex \
|
||||
node src/nostr-lock.mjs lock --agent docs-agent --cadence daily
|
||||
```
|
||||
@@ -0,0 +1,678 @@
|
||||
<!-- torch/dashboard/index.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-ds="new">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>TORCH Agent Dashboard</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg-surface text-text min-h-screen">
|
||||
<style>
|
||||
/*
|
||||
* Minimal overrides that cannot be expressed via Tailwind utilities.
|
||||
* Everything else uses design-token classes from tokens.css.
|
||||
*/
|
||||
.lock-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.pulse-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
animation: pulse-ring 2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes pulse-ring {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
.ttl-bar {
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
transition: width 1s linear;
|
||||
}
|
||||
.mono {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
"Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div role="main" class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
<!-- Header -->
|
||||
<header class="mb-8">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<h1 class="text-2xl font-semibold text-text-strong">
|
||||
TORCH Agent Dashboard
|
||||
</h1>
|
||||
<span
|
||||
id="connectionStatus"
|
||||
class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium bg-surface-alt text-muted"
|
||||
>
|
||||
<span class="pulse-dot bg-muted"></span>
|
||||
Connecting…
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-muted text-sm max-w-prose">
|
||||
Live view of
|
||||
<a
|
||||
href="../src/docs/TORCH.md"
|
||||
class="text-info hover:text-info-strong"
|
||||
>TORCH</a
|
||||
>
|
||||
lock events published by AI agents working on this TORCH project. Events
|
||||
are kind <code class="mono text-xs">30078</code> filtered by the
|
||||
<code id="introHashtag" class="mono text-xs">#torch-agent-lock</code> hashtag on public
|
||||
Nostr relays.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- Controls -->
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-4 mb-6 p-4 rounded-lg bg-surface-alt"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="cadenceFilter" class="text-sm font-medium text-text"
|
||||
>Cadence</label
|
||||
>
|
||||
<select
|
||||
id="cadenceFilter"
|
||||
class="rounded-md border border-border bg-surface px-3 py-1.5 text-sm text-text focus:outline-none focus:ring-2 focus:ring-accent"
|
||||
>
|
||||
<option value="all">All</option>
|
||||
<option value="daily" selected>Daily</option>
|
||||
<option value="weekly">Weekly</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="statusFilter" class="text-sm font-medium text-text"
|
||||
>Status</label
|
||||
>
|
||||
<select
|
||||
id="statusFilter"
|
||||
class="rounded-md border border-border bg-surface px-3 py-1.5 text-sm text-text focus:outline-none focus:ring-2 focus:ring-accent"
|
||||
>
|
||||
<option value="active" selected>Active only</option>
|
||||
<option value="all">All (incl. expired)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 ml-auto">
|
||||
<span id="eventCount" class="text-xs text-muted mono">0 events</span>
|
||||
<button
|
||||
id="refreshBtn"
|
||||
type="button"
|
||||
class="rounded-md bg-accent px-3 py-1.5 text-sm font-medium text-white hover:bg-accent-strong focus:outline-none focus:ring-2 focus:ring-accent transition-colors"
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary bar -->
|
||||
<div
|
||||
id="summaryBar"
|
||||
class="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-6"
|
||||
></div>
|
||||
|
||||
<!-- Lock cards -->
|
||||
<div id="lockGrid" class="lock-grid mb-8">
|
||||
<p id="emptyState" class="text-muted col-span-full text-center py-12">
|
||||
Waiting for lock events from relays…
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Raw event log -->
|
||||
<details class="rounded-lg border border-border">
|
||||
<summary
|
||||
class="cursor-pointer px-4 py-3 text-sm font-medium text-text hover:bg-surface-alt"
|
||||
>
|
||||
Raw event log
|
||||
</summary>
|
||||
<div
|
||||
id="rawLog"
|
||||
class="max-h-96 overflow-y-auto p-4 mono text-xs text-muted bg-surface-alt"
|
||||
>
|
||||
<p>Events will appear here as they arrive…</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Relay info -->
|
||||
<footer class="mt-8 pt-6 border-t border-border">
|
||||
<h2 class="text-sm font-semibold text-text mb-2">
|
||||
How this works
|
||||
</h2>
|
||||
<div class="text-xs text-muted space-y-1 max-w-prose">
|
||||
<p>
|
||||
This dashboard subscribes to Nostr relays for
|
||||
<strong>kind 30078</strong> events tagged with
|
||||
<code id="infoHashtag" class="mono">#torch-agent-lock</code>. These are the same
|
||||
events agents publish via
|
||||
<code class="mono">src/nostr-lock.mjs</code> when they
|
||||
claim tasks using the TORCH protocol.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Relays:</strong>
|
||||
<code id="relayList" class="mono">wss://relay.damus.io, wss://nos.lol, wss://relay.primal.net</code>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Filter:</strong>
|
||||
<code id="filterExample" class="mono">{"kinds":[30078],"#t":["torch-agent-lock"]}</code>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Unique identifier:</strong> All events use the
|
||||
<code id="dTagPrefix" class="mono">torch-lock/</code> namespace in the d-tag and
|
||||
the <code id="infoHashtagRepeat" class="mono">#torch-agent-lock</code> hashtag. This
|
||||
scopes events to this repository and is how both agents and this
|
||||
dashboard filter messages.
|
||||
</p>
|
||||
<p>
|
||||
To follow in any Nostr client, subscribe to kind 30078 with
|
||||
<code id="followHashtag" class="mono">#t = torch-agent-lock</code> on the relays
|
||||
above.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
// -----------------------------------------------------------------------
|
||||
// Configuration — matches src/nostr-lock.mjs defaults
|
||||
// Optional URL params:
|
||||
// ?namespace=torch&hashtag=torch-agent-lock&relays=wss://relay.damus.io,wss://nos.lol
|
||||
// -----------------------------------------------------------------------
|
||||
const DASHBOARD_DEFAULTS = {
|
||||
namespace: 'torch',
|
||||
hashtag: '',
|
||||
relays: [
|
||||
'wss://relay.damus.io',
|
||||
'wss://nos.lol',
|
||||
'wss://relay.primal.net',
|
||||
],
|
||||
};
|
||||
|
||||
function parseDashboardConfig() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const namespace =
|
||||
(params.get('namespace') || DASHBOARD_DEFAULTS.namespace).trim() ||
|
||||
DASHBOARD_DEFAULTS.namespace;
|
||||
const hashtag =
|
||||
(params.get('hashtag') || DASHBOARD_DEFAULTS.hashtag).trim() ||
|
||||
`${namespace}-agent-lock`;
|
||||
const relaysParam = (params.get('relays') || '').trim();
|
||||
const relays = relaysParam
|
||||
? relaysParam
|
||||
.split(',')
|
||||
.map((relay) => relay.trim())
|
||||
.filter(Boolean)
|
||||
: [...DASHBOARD_DEFAULTS.relays];
|
||||
|
||||
return {
|
||||
namespace,
|
||||
hashtag,
|
||||
relays,
|
||||
};
|
||||
}
|
||||
|
||||
const DASHBOARD_CONFIG = parseDashboardConfig();
|
||||
|
||||
const RELAYS = DASHBOARD_CONFIG.relays;
|
||||
const D_TAG_PREFIX = `${DASHBOARD_CONFIG.namespace}-lock/`;
|
||||
|
||||
const LOCK_EVENT_KIND = 30078;
|
||||
const HASHTAG = DASHBOARD_CONFIG.hashtag;
|
||||
|
||||
function applyConfigToHelpText() {
|
||||
const byId = (id) => document.getElementById(id);
|
||||
const hashtagDisplay = `#${HASHTAG}`;
|
||||
const filterExample = JSON.stringify({ kinds: [LOCK_EVENT_KIND], '#t': [HASHTAG] });
|
||||
|
||||
byId('introHashtag').textContent = hashtagDisplay;
|
||||
byId('infoHashtag').textContent = hashtagDisplay;
|
||||
byId('infoHashtagRepeat').textContent = hashtagDisplay;
|
||||
byId('followHashtag').textContent = `#t = ${HASHTAG}`;
|
||||
byId('dTagPrefix').textContent = D_TAG_PREFIX;
|
||||
byId('filterExample').textContent = filterExample;
|
||||
byId('relayList').textContent = RELAYS.join(', ');
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// State
|
||||
// -----------------------------------------------------------------------
|
||||
/** @type {Map<string, object>} eventId -> parsed lock */
|
||||
const lockStore = new Map();
|
||||
/** @type {WebSocket[]} */
|
||||
const sockets = [];
|
||||
let connectedCount = 0;
|
||||
let refreshIntervalId = null;
|
||||
let isTeardown = false;
|
||||
|
||||
// DOM refs
|
||||
const connectionStatus = document.getElementById('connectionStatus');
|
||||
const lockGrid = document.getElementById('lockGrid');
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
const rawLog = document.getElementById('rawLog');
|
||||
const eventCount = document.getElementById('eventCount');
|
||||
const summaryBar = document.getElementById('summaryBar');
|
||||
const cadenceFilter = document.getElementById('cadenceFilter');
|
||||
const statusFilter = document.getElementById('statusFilter');
|
||||
const refreshBtn = document.getElementById('refreshBtn');
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Cleanup check
|
||||
// -----------------------------------------------------------------------
|
||||
function isAlive() {
|
||||
if (isTeardown) return false;
|
||||
// Check if our root element is still in the DOM
|
||||
const stillInDom = document.body.contains(lockGrid);
|
||||
if (!stillInDom) {
|
||||
teardown();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function teardown() {
|
||||
if (isTeardown) return;
|
||||
isTeardown = true;
|
||||
|
||||
if (refreshIntervalId) {
|
||||
clearInterval(refreshIntervalId);
|
||||
refreshIntervalId = null;
|
||||
}
|
||||
|
||||
for (const ws of sockets) {
|
||||
try {
|
||||
ws.close();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
sockets.length = 0;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -----------------------------------------------------------------------
|
||||
function nowUnix() {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
function todayStr() {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function fmtTime(unixSec) {
|
||||
return new Date(unixSec * 1000).toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
function fmtDuration(seconds) {
|
||||
if (seconds < 0) return 'expired';
|
||||
const m = Math.floor(seconds / 60);
|
||||
if (m < 60) return `${m}m`;
|
||||
const h = Math.floor(m / 60);
|
||||
return `${h}h ${m % 60}m`;
|
||||
}
|
||||
|
||||
function parseLockEvent(event) {
|
||||
const dTag = (event.tags || []).find((t) => t[0] === 'd')?.[1] ?? '';
|
||||
const expTag = (event.tags || []).find(
|
||||
(t) => t[0] === 'expiration',
|
||||
)?.[1];
|
||||
const expiresAt = expTag ? parseInt(expTag, 10) : null;
|
||||
|
||||
let content = {};
|
||||
try {
|
||||
content = JSON.parse(event.content);
|
||||
} catch {
|
||||
// non-JSON content
|
||||
}
|
||||
|
||||
return {
|
||||
eventId: event.id,
|
||||
pubkey: event.pubkey,
|
||||
createdAt: event.created_at,
|
||||
expiresAt,
|
||||
dTag,
|
||||
agent: content.agent ?? dTag.split('/')[2] ?? 'unknown',
|
||||
cadence: content.cadence ?? dTag.split('/')[1] ?? 'unknown',
|
||||
status: content.status ?? 'unknown',
|
||||
date: content.date ?? dTag.split('/')[3] ?? todayStr(),
|
||||
platform: content.platform ?? 'unknown',
|
||||
};
|
||||
}
|
||||
|
||||
function isExpired(lock) {
|
||||
return lock.expiresAt != null && lock.expiresAt <= nowUnix();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Platform styling
|
||||
// -----------------------------------------------------------------------
|
||||
const PLATFORM_COLORS = {
|
||||
jules: { bg: 'bg-info/10', text: 'text-info', label: 'Jules' },
|
||||
'claude-code': {
|
||||
bg: 'bg-warning/10',
|
||||
text: 'text-warning',
|
||||
label: 'Claude Code',
|
||||
},
|
||||
codex: {
|
||||
bg: 'bg-success/10',
|
||||
text: 'text-success',
|
||||
label: 'Codex',
|
||||
},
|
||||
unknown: { bg: 'bg-muted/10', text: 'text-muted', label: 'Unknown' },
|
||||
};
|
||||
|
||||
function platformStyle(platform) {
|
||||
return PLATFORM_COLORS[platform] || PLATFORM_COLORS.unknown;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Rendering
|
||||
// -----------------------------------------------------------------------
|
||||
function getFilteredLocks() {
|
||||
const cadence = cadenceFilter.value;
|
||||
const showExpired = statusFilter.value === 'all';
|
||||
const now = nowUnix();
|
||||
|
||||
return [...lockStore.values()]
|
||||
.filter((l) => {
|
||||
if (cadence !== 'all' && l.cadence !== cadence) return false;
|
||||
if (!showExpired && isExpired(l)) return false;
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => b.createdAt - a.createdAt);
|
||||
}
|
||||
|
||||
function renderSummary(locks) {
|
||||
const now = nowUnix();
|
||||
const active = locks.filter((l) => !isExpired(l));
|
||||
const platforms = {};
|
||||
for (const l of active) {
|
||||
platforms[l.platform] = (platforms[l.platform] || 0) + 1;
|
||||
}
|
||||
|
||||
const cards = [
|
||||
{
|
||||
label: 'Active locks',
|
||||
value: active.length,
|
||||
color: 'text-accent-strong',
|
||||
},
|
||||
{
|
||||
label: 'Total events',
|
||||
value: locks.length,
|
||||
color: 'text-text',
|
||||
},
|
||||
{
|
||||
label: 'Platforms',
|
||||
value: Object.keys(platforms).join(', ') || 'none',
|
||||
color: 'text-info',
|
||||
},
|
||||
{
|
||||
label: 'Date',
|
||||
value: todayStr(),
|
||||
color: 'text-muted',
|
||||
},
|
||||
];
|
||||
|
||||
summaryBar.innerHTML = cards
|
||||
.map(
|
||||
(c) => `
|
||||
<div class="rounded-lg bg-surface-alt p-4">
|
||||
<div class="text-xs text-muted mb-1">${c.label}</div>
|
||||
<div class="text-lg font-semibold ${c.color} mono">${c.value}</div>
|
||||
</div>`,
|
||||
)
|
||||
.join('');
|
||||
}
|
||||
|
||||
function renderLocks() {
|
||||
if (!isAlive()) return;
|
||||
|
||||
const locks = getFilteredLocks();
|
||||
const now = nowUnix();
|
||||
|
||||
eventCount.textContent = `${lockStore.size} events`;
|
||||
|
||||
renderSummary(locks);
|
||||
|
||||
if (locks.length === 0) {
|
||||
lockGrid.innerHTML = '';
|
||||
lockGrid.appendChild(emptyState);
|
||||
emptyState.textContent =
|
||||
lockStore.size === 0
|
||||
? 'Waiting for lock events from relays\u2026'
|
||||
: 'No locks match the current filters.';
|
||||
return;
|
||||
}
|
||||
|
||||
lockGrid.innerHTML = locks
|
||||
.map((lock) => {
|
||||
const expired = isExpired(lock);
|
||||
const ps = platformStyle(lock.platform);
|
||||
const age = now - lock.createdAt;
|
||||
const remaining = lock.expiresAt ? lock.expiresAt - now : 0;
|
||||
const ttlTotal = lock.expiresAt
|
||||
? lock.expiresAt - lock.createdAt
|
||||
: 7200;
|
||||
const ttlPct = expired
|
||||
? 0
|
||||
: Math.max(0, Math.min(100, (remaining / ttlTotal) * 100));
|
||||
|
||||
return `
|
||||
<div class="rounded-lg border ${expired ? 'border-border/50 opacity-60' : 'border-border'} bg-surface-alt p-4 flex flex-col gap-3">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
${expired ? '' : '<span class="pulse-dot bg-success mt-1 shrink-0"></span>'}
|
||||
<div>
|
||||
<div class="font-medium text-text-strong text-sm">${lock.agent}</div>
|
||||
<div class="text-xs text-muted">${lock.cadence} · ${lock.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${ps.bg} ${ps.text}">
|
||||
${ps.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
|
||||
<div class="text-muted">Started</div>
|
||||
<div class="mono text-text">${fmtTime(lock.createdAt)}</div>
|
||||
<div class="text-muted">Age</div>
|
||||
<div class="mono text-text">${fmtDuration(age)}</div>
|
||||
<div class="text-muted">TTL remaining</div>
|
||||
<div class="mono ${expired ? 'text-danger' : 'text-text'}">${fmtDuration(remaining)}</div>
|
||||
<div class="text-muted">Status</div>
|
||||
<div class="mono ${expired ? 'text-danger' : 'text-success'}">${expired ? 'expired' : lock.status}</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full bg-border/30 rounded-full overflow-hidden">
|
||||
<div
|
||||
class="ttl-bar ${expired ? 'bg-danger' : 'bg-success'}"
|
||||
style="width: ${ttlPct}%"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-muted mono truncate" title="${lock.eventId}">
|
||||
${lock.eventId ? lock.eventId.slice(0, 16) + '\u2026' : 'no id'}
|
||||
</div>
|
||||
</div>`;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Raw log
|
||||
// -----------------------------------------------------------------------
|
||||
let rawLogInitialized = false;
|
||||
function appendRawLog(event) {
|
||||
if (!isAlive()) return;
|
||||
|
||||
if (!rawLogInitialized) {
|
||||
rawLog.innerHTML = '';
|
||||
rawLogInitialized = true;
|
||||
}
|
||||
const line = document.createElement('div');
|
||||
line.className = 'mb-2 pb-2 border-b border-border/30';
|
||||
const ts = new Date(event.created_at * 1000).toISOString();
|
||||
line.textContent = `[${ts}] ${JSON.stringify(event).slice(0, 300)}`;
|
||||
rawLog.prepend(line);
|
||||
|
||||
// Keep log bounded
|
||||
while (rawLog.children.length > 200) {
|
||||
rawLog.removeChild(rawLog.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// WebSocket relay connections
|
||||
// -----------------------------------------------------------------------
|
||||
function updateConnectionBadge() {
|
||||
if (!connectionStatus) return;
|
||||
const dot = connectionStatus.querySelector('.pulse-dot');
|
||||
if (connectedCount === RELAYS.length) {
|
||||
connectionStatus.className =
|
||||
'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium bg-success/10 text-success';
|
||||
dot.className = 'pulse-dot bg-success';
|
||||
connectionStatus.lastChild.textContent = ` ${connectedCount}/${RELAYS.length} relays`;
|
||||
} else if (connectedCount > 0) {
|
||||
connectionStatus.className =
|
||||
'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium bg-warning/10 text-warning';
|
||||
dot.className = 'pulse-dot bg-warning';
|
||||
connectionStatus.lastChild.textContent = ` ${connectedCount}/${RELAYS.length} relays`;
|
||||
} else {
|
||||
connectionStatus.className =
|
||||
'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium bg-danger/10 text-danger';
|
||||
dot.className = 'pulse-dot bg-danger';
|
||||
connectionStatus.lastChild.textContent = ' Disconnected';
|
||||
}
|
||||
}
|
||||
|
||||
function connectToRelay(url) {
|
||||
if (!isAlive()) return;
|
||||
|
||||
const ws = new WebSocket(url);
|
||||
const subId = 'torch-dash-' + Math.random().toString(36).slice(2, 8);
|
||||
|
||||
ws.addEventListener('open', () => {
|
||||
if (!isAlive()) return;
|
||||
connectedCount++;
|
||||
updateConnectionBadge();
|
||||
|
||||
// NIP-01 REQ: subscribe to TORCH lock events
|
||||
// Request from 7 days ago to cover active locks created before midnight
|
||||
// and recent history for weekly cadence
|
||||
const since = Math.floor(Date.now() / 1000) - 7 * 24 * 60 * 60;
|
||||
const filter = {
|
||||
kinds: [LOCK_EVENT_KIND],
|
||||
'#t': [HASHTAG],
|
||||
since: since,
|
||||
};
|
||||
ws.send(JSON.stringify(['REQ', subId, filter]));
|
||||
});
|
||||
|
||||
ws.addEventListener('message', (msg) => {
|
||||
if (!isAlive()) return;
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(msg.data);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
// NIP-01: ["EVENT", subId, event]
|
||||
if (data[0] === 'EVENT' && data[2]) {
|
||||
const event = data[2];
|
||||
const lock = parseLockEvent(event);
|
||||
|
||||
// De-duplicate: keep the event with earliest created_at per d-tag
|
||||
const existing = lockStore.get(lock.dTag);
|
||||
if (!existing || existing.createdAt > lock.createdAt) {
|
||||
lockStore.set(lock.dTag, lock);
|
||||
}
|
||||
|
||||
appendRawLog(event);
|
||||
renderLocks();
|
||||
}
|
||||
|
||||
// NIP-01: ["EOSE", subId] — end of stored events
|
||||
if (data[0] === 'EOSE') {
|
||||
renderLocks();
|
||||
}
|
||||
});
|
||||
|
||||
ws.addEventListener('close', () => {
|
||||
if (isTeardown) return;
|
||||
|
||||
connectedCount = Math.max(0, connectedCount - 1);
|
||||
updateConnectionBadge();
|
||||
|
||||
// Auto-reconnect after 5 seconds if still alive
|
||||
setTimeout(() => {
|
||||
if (isAlive()) {
|
||||
connectToRelay(url);
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
ws.addEventListener('error', () => {
|
||||
// Error fires before close, so close handler will decrement
|
||||
});
|
||||
|
||||
sockets.push(ws);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Periodic TTL refresh
|
||||
// -----------------------------------------------------------------------
|
||||
refreshIntervalId = setInterval(() => {
|
||||
if (isAlive()) {
|
||||
renderLocks();
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Event handlers
|
||||
// -----------------------------------------------------------------------
|
||||
if (cadenceFilter) cadenceFilter.addEventListener('change', renderLocks);
|
||||
if (statusFilter) statusFilter.addEventListener('change', renderLocks);
|
||||
if (refreshBtn) refreshBtn.addEventListener('click', () => {
|
||||
// Close existing connections and reconnect
|
||||
for (const ws of sockets) {
|
||||
try {
|
||||
ws.close();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
sockets.length = 0;
|
||||
connectedCount = 0;
|
||||
lockStore.clear();
|
||||
updateConnectionBadge();
|
||||
init();
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Init
|
||||
// -----------------------------------------------------------------------
|
||||
function init() {
|
||||
if (!isAlive()) return;
|
||||
|
||||
applyConfigToHelpText();
|
||||
|
||||
for (const url of RELAYS) {
|
||||
connectToRelay(url);
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,73 @@
|
||||
# Bitvid Agent Scheduler Meta Prompts (Example Overlay)
|
||||
|
||||
This file is an extracted **bitvid-specific overlay** for TORCH meta prompts.
|
||||
|
||||
Copy one block below into the agent session.
|
||||
|
||||
---
|
||||
|
||||
## Daily Scheduler Meta Prompt
|
||||
|
||||
```text
|
||||
You are the bitvid daily agent scheduler.
|
||||
|
||||
Follow `src/prompts/scheduler-flow.md` exactly.
|
||||
|
||||
MUST 1: Set cadence config to:
|
||||
- cadence = daily
|
||||
- log_dir = task-logs/daily/
|
||||
- branch_prefix = agents/daily/
|
||||
- prompt_dir = src/prompts/daily/
|
||||
|
||||
MUST 2: Run preflight to get the exclusion set:
|
||||
|
||||
node src/nostr-lock.mjs check --cadence daily
|
||||
|
||||
Use the `locked` array from the JSON output as the exclusion set.
|
||||
|
||||
MUST 3: Run these commands in this order:
|
||||
1) cat AGENTS.md CLAUDE.md
|
||||
2) ls -1 task-logs/daily/ | sort | tail -n 1
|
||||
3) Select next roster agent not in exclusion set
|
||||
4) Claim via Nostr lock:
|
||||
AGENT_PLATFORM=jules node src/nostr-lock.mjs lock --agent <agent-name> --cadence daily
|
||||
Exit 0 = lock acquired, proceed. Exit 3 = race lost, go back to step 2.
|
||||
5) Execute selected prompt from src/prompts/daily/
|
||||
6) npm run lint
|
||||
7) Create `_completed.md` or `_failed.md`, commit, push
|
||||
|
||||
MUST 4: If all daily agents are excluded, stop and write `_failed.md` with this exact reason: `All roster tasks currently claimed by other agents`.
|
||||
```
|
||||
|
||||
## Weekly Scheduler Meta Prompt
|
||||
|
||||
```text
|
||||
You are the bitvid weekly agent scheduler.
|
||||
|
||||
Follow `src/prompts/scheduler-flow.md` exactly.
|
||||
|
||||
MUST 1: Set cadence config to:
|
||||
- cadence = weekly
|
||||
- log_dir = task-logs/weekly/
|
||||
- branch_prefix = agents/weekly/
|
||||
- prompt_dir = src/prompts/weekly/
|
||||
|
||||
MUST 2: Run preflight to get the exclusion set:
|
||||
|
||||
node src/nostr-lock.mjs check --cadence weekly
|
||||
|
||||
Use the `locked` array from the JSON output as the exclusion set.
|
||||
|
||||
MUST 3: Run these commands in this order:
|
||||
1) cat AGENTS.md CLAUDE.md
|
||||
2) ls -1 task-logs/weekly/ | sort | tail -n 1
|
||||
3) Select next roster agent not in exclusion set
|
||||
4) Claim via Nostr lock:
|
||||
AGENT_PLATFORM=jules node src/nostr-lock.mjs lock --agent <agent-name> --cadence weekly
|
||||
Exit 0 = lock acquired, proceed. Exit 3 = race lost, go back to step 2.
|
||||
5) Execute selected prompt from src/prompts/weekly/
|
||||
6) npm run lint
|
||||
7) Create `_completed.md` or `_failed.md`, commit, push
|
||||
|
||||
MUST 4: If all weekly agents are excluded, stop and write `_failed.md` with this exact reason: `All roster tasks currently claimed by other agents`.
|
||||
```
|
||||
@@ -0,0 +1,38 @@
|
||||
# Daily Agent Scheduler Prompt (Bitvid Example Overlay)
|
||||
|
||||
This file is an extracted **bitvid-specific overlay** for TORCH scheduler prompts.
|
||||
|
||||
Use `src/prompts/scheduler-flow.md` as the authoritative scheduler procedure.
|
||||
|
||||
## Daily Cadence Configuration (Bitvid Example)
|
||||
|
||||
- `cadence`: `daily`
|
||||
- `log_dir`: `task-logs/daily/`
|
||||
- `branch_prefix`: `agents/daily/`
|
||||
- `prompt_dir`: `src/prompts/daily/`
|
||||
|
||||
## Daily Agent Roster (alphabetical order)
|
||||
|
||||
| # | Agent Name | Prompt File |
|
||||
|---|-----------|-------------|
|
||||
| 1 | audit-agent | `bitvid-audit-agent.md` |
|
||||
| 2 | ci-health-agent | `bitvid-ci-health-agent.md` |
|
||||
| 3 | const-refactor-agent | `bitvid-const-refactor-agent.md` |
|
||||
| 4 | content-audit-agent | `bitvid-content-audit-agent.md` |
|
||||
| 5 | decompose-agent | `bitvid-decompose-agent.md` |
|
||||
| 6 | deps-security-agent | `bitvid-deps-security-agent.md` |
|
||||
| 7 | design-system-audit-agent | `bitvid-design-system-audit-agent.md` |
|
||||
| 8 | docs-agent | `bitvid-docs-agent.md` |
|
||||
| 9 | docs-alignment-agent | `bitvid-docs-alignment-agent.md` |
|
||||
| 10 | docs-code-investigator | `bitvid-docs-code-investigator.md` |
|
||||
| 11 | innerhtml-migration-agent | `bitvid-innerhtml-migration-agent.md` |
|
||||
| 12 | known-issues-agent | `bitvid-known-issues-agent.md` |
|
||||
| 13 | load-test-agent | `bitvid-load-test-agent.md` |
|
||||
| 14 | nip-research-agent | `bitvid-nip-research-agent.md` |
|
||||
| 15 | onboarding-audit-agent | `bitvid-onboarding-audit-agent.md` |
|
||||
| 16 | perf-agent | `bitvid-perf-agent.md` |
|
||||
| 17 | prompt-curator-agent | `bitvid-prompt-curator-agent.md` |
|
||||
| 18 | scheduler-update-agent | `bitvid-scheduler-update-agent.md` |
|
||||
| 19 | style-agent | `bitvid-style-agent.md` |
|
||||
| 20 | test-audit-agent | `bitvid-test-audit-agent.md` |
|
||||
| 21 | todo-triage-agent | `bitvid-todo-triage-agent.md` |
|
||||
@@ -0,0 +1,71 @@
|
||||
# Scheduler Flow (Bitvid Example Overlay)
|
||||
|
||||
This file is an extracted **bitvid-specific overlay** for TORCH scheduler flow.
|
||||
|
||||
Use this document for **all scheduler runs**. Task locking uses the **TORCH** protocol (see `src/docs/TORCH.md`).
|
||||
|
||||
## Scheduler Override (Top-Line Rule)
|
||||
|
||||
During scheduler execution, the scheduler-specific instructions in this document and the cadence prompt (`daily-scheduler.md` or `weekly-scheduler.md`) **supersede generic AGENTS workflow details** when they conflict.
|
||||
|
||||
### Global rules that still apply
|
||||
|
||||
- Follow core safety and policy constraints (no harmful behavior, no secret exfiltration, no policy violations).
|
||||
- Do not perform destructive or irreversible actions unless explicitly required by the scheduler task definition.
|
||||
- Keep repository integrity checks (e.g., required lint/test commands in the scheduler flow) and leave an auditable log trail.
|
||||
- Respect non-interactive execution constraints and do not pause for manual approvals.
|
||||
|
||||
### Generic AGENTS workflow details intentionally ignored for scheduler runs
|
||||
|
||||
- The normal "one subsystem per PR" restriction, because scheduler operations are orchestration/meta-work that may touch scheduler logs, prompts, and coordination docs together.
|
||||
- The standard task-claim sequence described for general agents when it conflicts with the stricter, numbered MUST sequence below.
|
||||
- Generic start-of-task bookkeeping patterns not referenced by scheduler prompts (for example, creating extra context/todo artifacts) when they would add noise to automated scheduler runs.
|
||||
|
||||
## Numbered MUST Procedure
|
||||
|
||||
1. **MUST** set cadence variables before any command:
|
||||
- `cadence` = `daily` or `weekly`
|
||||
- `log_dir` = `task-logs/<cadence>/`
|
||||
- `branch_prefix` = `agents/<cadence>/`
|
||||
- `prompt_dir` = `src/prompts/<cadence>/`
|
||||
|
||||
2. **MUST** run the preflight check to build the exclusion set:
|
||||
|
||||
```bash
|
||||
node src/nostr-lock.mjs check --cadence <cadence>
|
||||
```
|
||||
|
||||
This queries Nostr relays for active lock events and returns JSON with `locked` and `available` agent lists. Use the `locked` array as the exclusion set.
|
||||
|
||||
3. **MUST** read both policy files before selecting an agent:
|
||||
|
||||
```bash
|
||||
cat AGENTS.md CLAUDE.md
|
||||
```
|
||||
|
||||
4. **MUST** run this command to identify the latest cadence log file, then choose the next roster agent not in the exclusion set:
|
||||
|
||||
```bash
|
||||
ls -1 <log_dir> | sort | tail -n 1
|
||||
```
|
||||
|
||||
5. **MUST** stop and write a `_failed.md` log with `All roster tasks currently claimed by other agents` when every roster agent is excluded.
|
||||
|
||||
6. **MUST** claim the selected agent:
|
||||
|
||||
```bash
|
||||
AGENT_PLATFORM=<jules|claude-code|codex> \
|
||||
node src/nostr-lock.mjs lock \
|
||||
--agent <agent-name> \
|
||||
--cadence <cadence>
|
||||
```
|
||||
|
||||
The script generates an ephemeral keypair, publishes a NIP-78 lock event to Nostr relays with NIP-40 auto-expiration (2 hours), and performs a built-in race check. No tokens or secrets needed.
|
||||
|
||||
- **Exit code 0** = lock acquired, proceed to Step 7.
|
||||
- **Exit code 3** = race lost (another agent claimed first). Return to Step 2.
|
||||
- **Exit code 2** = relay error. Write a `_failed.md` log with reason `Nostr relay error` and stop.
|
||||
|
||||
7. **MUST** execute `<prompt_dir>/<prompt-file>` end-to-end.
|
||||
|
||||
8. **MUST** create exactly one final status file (`_completed.md` or `_failed.md`), run `npm run lint`, then commit and push.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Weekly Agent Scheduler Prompt (Bitvid Example Overlay)
|
||||
|
||||
This file is an extracted **bitvid-specific overlay** for TORCH scheduler prompts.
|
||||
|
||||
Use `src/prompts/scheduler-flow.md` as the authoritative scheduler procedure.
|
||||
|
||||
## Weekly Cadence Configuration (Bitvid Example)
|
||||
|
||||
- `cadence`: `weekly`
|
||||
- `log_dir`: `task-logs/weekly/`
|
||||
- `branch_prefix`: `agents/weekly/`
|
||||
- `prompt_dir`: `src/prompts/weekly/`
|
||||
|
||||
## Weekly Agent Roster (alphabetical order)
|
||||
|
||||
| # | Agent Name | Prompt File |
|
||||
|---|-----------|-------------|
|
||||
| 1 | bug-reproducer-agent | `bitvid-bug-reproducer-agent.md` |
|
||||
| 2 | changelog-agent | `bitvid-changelog-agent.md` |
|
||||
| 3 | dead-code-agent | `bitvid-dead-code-agent.md` |
|
||||
| 4 | event-schema-agent | `bitvid-event-schema-agent.md` |
|
||||
| 5 | frontend-console-debug-agent | `bitvid-frontend-console-debug-agent.md` |
|
||||
| 6 | fuzz-agent | `bitvid-fuzz-agent.md` |
|
||||
| 7 | interop-agent | `bitvid-interop-agent.md` |
|
||||
| 8 | perf-deepdive-agent | `bitvid-perf-deepdive-agent.md` |
|
||||
| 9 | perf-optimization-agent | `bitvid-perf-optimization-agent.md` |
|
||||
| 10 | pr-review-agent | `bitvid-pr-review-agent.md` |
|
||||
| 11 | race-condition-agent | `bitvid-race-condition-agent.md` |
|
||||
| 12 | refactor-agent | `bitvid-refactor-agent.md` |
|
||||
| 13 | smoke-agent | `bitvid-smoke-agent.md` |
|
||||
| 14 | telemetry-agent | `bitvid-telemetry-agent.md` |
|
||||
| 15 | test-coverage-agent | `bitvid-test-coverage-agent.md` |
|
||||
| 16 | weekly-synthesis-agent | `bitvid-weekly-synthesis-agent.md` |
|
||||
Generated
+162
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"name": "torch-lock",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "torch-lock",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"nostr-tools": "2.19.4",
|
||||
"ws": "8.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/ciphers": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz",
|
||||
"integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/curves": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
|
||||
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "1.3.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/curves/node_modules/@noble/hashes": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
|
||||
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
|
||||
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@scure/base": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
|
||||
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@scure/bip32": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz",
|
||||
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/curves": "~1.1.0",
|
||||
"@noble/hashes": "~1.3.1",
|
||||
"@scure/base": "~1.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@scure/bip32/node_modules/@noble/curves": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz",
|
||||
"integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "1.3.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@scure/bip39": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz",
|
||||
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "~1.3.0",
|
||||
"@scure/base": "~1.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/nostr-tools": {
|
||||
"version": "2.19.4",
|
||||
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.19.4.tgz",
|
||||
"integrity": "sha512-qVLfoTpZegNYRJo5j+Oi6RPu0AwLP6jcvzcB3ySMnIT5DrAGNXfs5HNBspB/2HiGfH3GY+v6yXkTtcKSBQZwSg==",
|
||||
"license": "Unlicense",
|
||||
"dependencies": {
|
||||
"@noble/ciphers": "^0.5.1",
|
||||
"@noble/curves": "1.2.0",
|
||||
"@noble/hashes": "1.3.1",
|
||||
"@scure/base": "1.1.1",
|
||||
"@scure/bip32": "1.3.1",
|
||||
"@scure/bip39": "1.2.1",
|
||||
"nostr-wasm": "0.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/nostr-wasm": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz",
|
||||
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.19.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "torch-lock",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "TORCH standalone lock CLI and optional dashboard",
|
||||
"scripts": {
|
||||
"lock:check:daily": "node src/nostr-lock.mjs check --cadence daily",
|
||||
"lock:check:weekly": "node src/nostr-lock.mjs check --cadence weekly",
|
||||
"lock:list": "node src/nostr-lock.mjs list",
|
||||
"lock:lock": "node src/nostr-lock.mjs lock",
|
||||
"dashboard:serve": "python3 -m http.server 4173"
|
||||
},
|
||||
"dependencies": {
|
||||
"nostr-tools": "2.19.4",
|
||||
"ws": "8.19.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
# TORCH: Task Orchestration via Relay-Coordinated Handoff
|
||||
|
||||
TORCH is a decentralized task-locking protocol for multi-agent software development.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# Check active locks
|
||||
node src/nostr-lock.mjs check --cadence daily
|
||||
|
||||
# Claim a task
|
||||
AGENT_PLATFORM=codex node src/nostr-lock.mjs lock --agent docs-agent --cadence daily
|
||||
|
||||
# List all active locks
|
||||
node src/nostr-lock.mjs list
|
||||
```
|
||||
|
||||
## Defaults
|
||||
|
||||
- Kind: `30078` (NIP-33 parameterized replaceable)
|
||||
- Expiration: `NIP-40` via `expiration` tag
|
||||
- TTL: `7200s` (2h)
|
||||
- Namespace: `torch`
|
||||
- Relays:
|
||||
- `wss://relay.damus.io`
|
||||
- `wss://nos.lol`
|
||||
- `wss://relay.primal.net`
|
||||
|
||||
## Environment variables
|
||||
|
||||
- `NOSTR_LOCK_NAMESPACE`
|
||||
- `NOSTR_LOCK_RELAYS`
|
||||
- `NOSTR_LOCK_TTL`
|
||||
- `NOSTR_LOCK_DAILY_ROSTER`
|
||||
- `NOSTR_LOCK_WEEKLY_ROSTER`
|
||||
- `AGENT_PLATFORM`
|
||||
|
||||
## Exit codes
|
||||
|
||||
- `0`: success
|
||||
- `1`: usage error
|
||||
- `2`: relay/network error
|
||||
- `3`: lock denied (already locked or race lost)
|
||||
@@ -0,0 +1,454 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// TORCH — Task Orchestration via Relay-Coordinated Handoff
|
||||
// Generic Nostr-based task locking for multi-agent development.
|
||||
|
||||
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
|
||||
import { SimplePool } from 'nostr-tools/pool';
|
||||
import { useWebSocketImplementation } from 'nostr-tools/relay';
|
||||
import WebSocket from 'ws';
|
||||
|
||||
useWebSocketImplementation(WebSocket);
|
||||
|
||||
const DEFAULT_RELAYS = [
|
||||
'wss://relay.damus.io',
|
||||
'wss://nos.lol',
|
||||
'wss://relay.primal.net',
|
||||
];
|
||||
|
||||
const DEFAULT_TTL = 7200;
|
||||
const DEFAULT_NAMESPACE = 'torch';
|
||||
const QUERY_TIMEOUT_MS = 15_000;
|
||||
const VALID_CADENCES = new Set(['daily', 'weekly']);
|
||||
|
||||
const DEFAULT_DAILY_ROSTER = [
|
||||
'documentation-agent',
|
||||
'quality-agent',
|
||||
'security-agent',
|
||||
'performance-agent',
|
||||
'refactor-agent',
|
||||
];
|
||||
|
||||
const DEFAULT_WEEKLY_ROSTER = [
|
||||
'bug-reproducer-agent',
|
||||
'integration-agent',
|
||||
'release-agent',
|
||||
'test-coverage-agent',
|
||||
'weekly-synthesis-agent',
|
||||
];
|
||||
|
||||
function getRelays() {
|
||||
const envRelays = process.env.NOSTR_LOCK_RELAYS;
|
||||
if (envRelays) {
|
||||
return envRelays.split(',').map((r) => r.trim()).filter(Boolean);
|
||||
}
|
||||
return DEFAULT_RELAYS;
|
||||
}
|
||||
|
||||
function getNamespace() {
|
||||
const namespace = (process.env.NOSTR_LOCK_NAMESPACE || DEFAULT_NAMESPACE).trim();
|
||||
return namespace || DEFAULT_NAMESPACE;
|
||||
}
|
||||
|
||||
function getTtl() {
|
||||
const envTtl = process.env.NOSTR_LOCK_TTL;
|
||||
if (envTtl) {
|
||||
const parsed = parseInt(envTtl, 10);
|
||||
if (!Number.isNaN(parsed) && parsed > 0) return parsed;
|
||||
}
|
||||
return DEFAULT_TTL;
|
||||
}
|
||||
|
||||
function parseEnvRoster(value) {
|
||||
if (!value) return null;
|
||||
return value
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function getRoster(cadence) {
|
||||
const dailyFromEnv = parseEnvRoster(process.env.NOSTR_LOCK_DAILY_ROSTER);
|
||||
const weeklyFromEnv = parseEnvRoster(process.env.NOSTR_LOCK_WEEKLY_ROSTER);
|
||||
|
||||
if (cadence === 'daily') {
|
||||
return dailyFromEnv && dailyFromEnv.length ? dailyFromEnv : DEFAULT_DAILY_ROSTER;
|
||||
}
|
||||
|
||||
return weeklyFromEnv && weeklyFromEnv.length ? weeklyFromEnv : DEFAULT_WEEKLY_ROSTER;
|
||||
}
|
||||
|
||||
function todayDateStr() {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function nowUnix() {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
function parseLockEvent(event) {
|
||||
const dTag = event.tags.find((t) => t[0] === 'd')?.[1] ?? '';
|
||||
const expTag = event.tags.find((t) => t[0] === 'expiration')?.[1];
|
||||
const expiresAt = expTag ? parseInt(expTag, 10) : null;
|
||||
|
||||
let content = {};
|
||||
try {
|
||||
content = JSON.parse(event.content);
|
||||
} catch {
|
||||
// Ignore malformed JSON content
|
||||
}
|
||||
|
||||
return {
|
||||
eventId: event.id,
|
||||
pubkey: event.pubkey,
|
||||
createdAt: event.created_at,
|
||||
createdAtIso: new Date(event.created_at * 1000).toISOString(),
|
||||
expiresAt,
|
||||
expiresAtIso: expiresAt ? new Date(expiresAt * 1000).toISOString() : null,
|
||||
dTag,
|
||||
agent: content.agent ?? null,
|
||||
cadence: content.cadence ?? null,
|
||||
status: content.status ?? null,
|
||||
date: content.date ?? null,
|
||||
platform: content.platform ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function filterActiveLocks(locks) {
|
||||
const now = nowUnix();
|
||||
return locks.filter((lock) => !lock.expiresAt || lock.expiresAt > now);
|
||||
}
|
||||
|
||||
async function queryLocks(relays, cadence, dateStr, namespace) {
|
||||
const pool = new SimplePool();
|
||||
const tagFilter = `${namespace}-lock-${cadence}-${dateStr}`;
|
||||
|
||||
try {
|
||||
const events = await Promise.race([
|
||||
pool.querySync(relays, {
|
||||
kinds: [30078],
|
||||
'#t': [tagFilter],
|
||||
}),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Relay query timed out')), QUERY_TIMEOUT_MS),
|
||||
),
|
||||
]);
|
||||
|
||||
return filterActiveLocks(events.map(parseLockEvent));
|
||||
} finally {
|
||||
pool.close(relays);
|
||||
}
|
||||
}
|
||||
|
||||
async function publishLock(relays, event) {
|
||||
const pool = new SimplePool();
|
||||
|
||||
try {
|
||||
const publishPromises = pool.publish(relays, event);
|
||||
const results = await Promise.allSettled(publishPromises);
|
||||
const successes = results.filter((r) => r.status === 'fulfilled');
|
||||
|
||||
if (successes.length === 0) {
|
||||
const errors = results.map((r, i) => {
|
||||
if (r.status === 'rejected') {
|
||||
const reason = r.reason;
|
||||
const message = reason instanceof Error ? reason.message : String(reason ?? 'unknown');
|
||||
return `${relays[i]}: ${message}`;
|
||||
}
|
||||
return `${relays[i]}: unknown`;
|
||||
});
|
||||
throw new Error(`Failed to publish to any relay:\n ${errors.join('\n ')}`);
|
||||
}
|
||||
|
||||
console.error(` Published to ${successes.length}/${relays.length} relays`);
|
||||
return event;
|
||||
} finally {
|
||||
pool.close(relays);
|
||||
}
|
||||
}
|
||||
|
||||
async function cmdCheck(cadence) {
|
||||
const relays = getRelays();
|
||||
const namespace = getNamespace();
|
||||
const dateStr = todayDateStr();
|
||||
|
||||
console.error(`Checking locks: namespace=${namespace}, cadence=${cadence}, date=${dateStr}`);
|
||||
console.error(`Relays: ${relays.join(', ')}`);
|
||||
|
||||
const locks = await queryLocks(relays, cadence, dateStr, namespace);
|
||||
const lockedAgents = [...new Set(locks.map((l) => l.agent).filter(Boolean))];
|
||||
const roster = getRoster(cadence);
|
||||
const available = roster.filter((a) => !lockedAgents.includes(a));
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
namespace,
|
||||
cadence,
|
||||
date: dateStr,
|
||||
locked: lockedAgents.sort(),
|
||||
available: available.sort(),
|
||||
lockCount: locks.length,
|
||||
locks: locks.map((l) => ({
|
||||
agent: l.agent,
|
||||
eventId: l.eventId,
|
||||
createdAt: l.createdAtIso,
|
||||
expiresAt: l.expiresAtIso,
|
||||
platform: l.platform,
|
||||
})),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async function cmdLock(agent, cadence, dryRun = false) {
|
||||
const relays = getRelays();
|
||||
const namespace = getNamespace();
|
||||
const dateStr = todayDateStr();
|
||||
const ttl = getTtl();
|
||||
const now = nowUnix();
|
||||
const expiresAt = now + ttl;
|
||||
|
||||
console.error(`Locking: namespace=${namespace}, agent=${agent}, cadence=${cadence}, date=${dateStr}`);
|
||||
console.error(`TTL: ${ttl}s, expires: ${new Date(expiresAt * 1000).toISOString()}`);
|
||||
console.error(`Relays: ${relays.join(', ')}`);
|
||||
|
||||
console.error('Step 1: Checking for existing locks...');
|
||||
const existingLocks = await queryLocks(relays, cadence, dateStr, namespace);
|
||||
const conflicting = existingLocks.filter((l) => l.agent === agent);
|
||||
|
||||
if (conflicting.length > 0) {
|
||||
const earliest = conflicting.sort((a, b) => a.createdAt - b.createdAt)[0];
|
||||
console.error(
|
||||
`LOCK DENIED: ${agent} already locked by event ${earliest.eventId} ` +
|
||||
`(created ${earliest.createdAtIso}, platform: ${earliest.platform})`,
|
||||
);
|
||||
console.log('LOCK_STATUS=denied');
|
||||
console.log('LOCK_REASON=already_locked');
|
||||
console.log(`LOCK_EXISTING_EVENT=${earliest.eventId}`);
|
||||
process.exit(3);
|
||||
}
|
||||
|
||||
console.error('Step 2: Generating ephemeral keypair...');
|
||||
const sk = generateSecretKey();
|
||||
const pk = getPublicKey(sk);
|
||||
console.error(` Ephemeral pubkey: ${pk.slice(0, 16)}...`);
|
||||
|
||||
console.error('Step 3: Building lock event...');
|
||||
const event = finalizeEvent(
|
||||
{
|
||||
kind: 30078,
|
||||
created_at: now,
|
||||
tags: [
|
||||
['d', `${namespace}-lock/${cadence}/${agent}/${dateStr}`],
|
||||
['t', `${namespace}-agent-lock`],
|
||||
['t', `${namespace}-lock-${cadence}`],
|
||||
['t', `${namespace}-lock-${cadence}-${dateStr}`],
|
||||
['expiration', String(expiresAt)],
|
||||
],
|
||||
content: JSON.stringify({
|
||||
agent,
|
||||
cadence,
|
||||
status: 'started',
|
||||
namespace,
|
||||
date: dateStr,
|
||||
platform: process.env.AGENT_PLATFORM || 'unknown',
|
||||
lockedAt: new Date(now * 1000).toISOString(),
|
||||
expiresAt: new Date(expiresAt * 1000).toISOString(),
|
||||
}),
|
||||
},
|
||||
sk,
|
||||
);
|
||||
|
||||
console.error(` Event ID: ${event.id}`);
|
||||
|
||||
if (dryRun) {
|
||||
console.error('Step 4: [DRY RUN] Skipping publish — event built but not sent');
|
||||
console.error('RACE CHECK: won (dry run — no real contention possible)');
|
||||
} else {
|
||||
console.error('Step 4: Publishing to relays...');
|
||||
await publishLock(relays, event);
|
||||
|
||||
console.error('Step 5: Race check...');
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
|
||||
const postLocks = await queryLocks(relays, cadence, dateStr, namespace);
|
||||
const racingLocks = postLocks
|
||||
.filter((l) => l.agent === agent)
|
||||
.sort((a, b) => a.createdAt - b.createdAt);
|
||||
|
||||
if (racingLocks.length > 1 && racingLocks[0].eventId !== event.id) {
|
||||
const winner = racingLocks[0];
|
||||
console.error(
|
||||
`RACE CHECK: lost (earlier lock by event ${winner.eventId}, created ${winner.createdAtIso})`,
|
||||
);
|
||||
console.log('LOCK_STATUS=race_lost');
|
||||
console.log('LOCK_REASON=earlier_claim_exists');
|
||||
console.log(`LOCK_WINNER_EVENT=${winner.eventId}`);
|
||||
process.exit(3);
|
||||
}
|
||||
|
||||
console.error('RACE CHECK: won');
|
||||
}
|
||||
|
||||
console.log('LOCK_STATUS=ok');
|
||||
console.log(`LOCK_EVENT_ID=${event.id}`);
|
||||
console.log(`LOCK_PUBKEY=${pk}`);
|
||||
console.log(`LOCK_AGENT=${agent}`);
|
||||
console.log(`LOCK_CADENCE=${cadence}`);
|
||||
console.log(`LOCK_DATE=${dateStr}`);
|
||||
console.log(`LOCK_EXPIRES=${expiresAt}`);
|
||||
console.log(`LOCK_EXPIRES_ISO=${new Date(expiresAt * 1000).toISOString()}`);
|
||||
}
|
||||
|
||||
async function cmdList(cadence) {
|
||||
const relays = getRelays();
|
||||
const namespace = getNamespace();
|
||||
const dateStr = todayDateStr();
|
||||
const cadences = cadence ? [cadence] : ['daily', 'weekly'];
|
||||
|
||||
console.error(`Listing active locks: namespace=${namespace}, cadences=${cadences.join(', ')}`);
|
||||
|
||||
for (const c of cadences) {
|
||||
const locks = await queryLocks(relays, c, dateStr, namespace);
|
||||
|
||||
console.log(`\n${'='.repeat(72)}`);
|
||||
console.log(`Active ${namespace} ${c} locks (${dateStr})`);
|
||||
console.log('='.repeat(72));
|
||||
|
||||
if (locks.length === 0) {
|
||||
console.log(' (no active locks)');
|
||||
continue;
|
||||
}
|
||||
|
||||
const sorted = locks.sort((a, b) => a.createdAt - b.createdAt);
|
||||
for (const lock of sorted) {
|
||||
const age = nowUnix() - lock.createdAt;
|
||||
const ageMin = Math.round(age / 60);
|
||||
const remaining = lock.expiresAt ? lock.expiresAt - nowUnix() : null;
|
||||
const remainMin = remaining ? Math.round(remaining / 60) : '?';
|
||||
|
||||
console.log(
|
||||
` ${(lock.agent ?? 'unknown').padEnd(30)} ` +
|
||||
`age: ${String(ageMin).padStart(4)}m ` +
|
||||
`ttl: ${String(remainMin).padStart(4)}m ` +
|
||||
`platform: ${lock.platform ?? '?'} ` +
|
||||
`event: ${lock.eventId?.slice(0, 12)}...`,
|
||||
);
|
||||
}
|
||||
|
||||
const roster = getRoster(c);
|
||||
const lockedAgents = new Set(locks.map((l) => l.agent).filter(Boolean));
|
||||
const available = roster.filter((a) => !lockedAgents.has(a));
|
||||
|
||||
console.log(`\n Locked: ${lockedAgents.size}/${roster.length}`);
|
||||
console.log(` Available: ${available.join(', ') || '(none)'}`);
|
||||
}
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = { command: null, agent: null, cadence: null, dryRun: false };
|
||||
let i = 0;
|
||||
|
||||
if (argv.length > 0 && !argv[0].startsWith('-')) {
|
||||
args.command = argv[0];
|
||||
i = 1;
|
||||
}
|
||||
|
||||
for (; i < argv.length; i++) {
|
||||
const arg = argv[i];
|
||||
if (arg === '--agent' || arg === '-a') {
|
||||
args.agent = argv[++i];
|
||||
} else if (arg === '--cadence' || arg === '-c') {
|
||||
args.cadence = argv[++i];
|
||||
} else if (arg.startsWith('--agent=')) {
|
||||
args.agent = arg.split('=')[1];
|
||||
} else if (arg.startsWith('--cadence=')) {
|
||||
args.cadence = arg.split('=')[1];
|
||||
} else if (arg === '--dry-run') {
|
||||
args.dryRun = true;
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
function usage() {
|
||||
console.error(`Usage: node src/nostr-lock.mjs <command> [options]
|
||||
|
||||
Commands:
|
||||
check --cadence <daily|weekly> Check locked agents (JSON)
|
||||
lock --agent <name> --cadence <daily|weekly> Claim a lock
|
||||
list [--cadence <daily|weekly>] Print active lock table
|
||||
|
||||
Options:
|
||||
--dry-run Build and sign the event but do not publish
|
||||
|
||||
Environment:
|
||||
NOSTR_LOCK_NAMESPACE Namespace prefix for lock tags (default: torch)
|
||||
NOSTR_LOCK_RELAYS Comma-separated relay WSS URLs
|
||||
NOSTR_LOCK_TTL Lock TTL in seconds (default: 7200)
|
||||
NOSTR_LOCK_DAILY_ROSTER Comma-separated daily roster (optional)
|
||||
NOSTR_LOCK_WEEKLY_ROSTER Comma-separated weekly roster (optional)
|
||||
AGENT_PLATFORM Platform identifier (e.g., codex)
|
||||
|
||||
Exit codes:
|
||||
0 Success
|
||||
1 Usage error
|
||||
2 Relay/network error
|
||||
3 Lock denied (already locked or race lost)`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
if (!args.command) {
|
||||
usage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
switch (args.command) {
|
||||
case 'check': {
|
||||
if (!args.cadence || !VALID_CADENCES.has(args.cadence)) {
|
||||
console.error('ERROR: --cadence <daily|weekly> is required for check');
|
||||
process.exit(1);
|
||||
}
|
||||
await cmdCheck(args.cadence);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'lock': {
|
||||
if (!args.agent) {
|
||||
console.error('ERROR: --agent <name> is required for lock');
|
||||
process.exit(1);
|
||||
}
|
||||
if (!args.cadence || !VALID_CADENCES.has(args.cadence)) {
|
||||
console.error('ERROR: --cadence <daily|weekly> is required for lock');
|
||||
process.exit(1);
|
||||
}
|
||||
await cmdLock(args.agent, args.cadence, args.dryRun);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'list': {
|
||||
if (args.cadence && !VALID_CADENCES.has(args.cadence)) {
|
||||
console.error('ERROR: --cadence must be daily or weekly');
|
||||
process.exit(1);
|
||||
}
|
||||
await cmdList(args.cadence || null);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.error(`ERROR: Unknown command: ${args.command}`);
|
||||
usage();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(`nostr-lock failed: ${err.message}`);
|
||||
process.exit(2);
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
# Agent Scheduler Meta Prompts (Generic)
|
||||
|
||||
Copy one block below into the scheduler agent session.
|
||||
|
||||
---
|
||||
|
||||
## Daily Scheduler Meta Prompt
|
||||
|
||||
```text
|
||||
You are the daily agent scheduler for this repository.
|
||||
|
||||
Follow `src/prompts/scheduler-flow.md` exactly.
|
||||
|
||||
MUST 1: Set cadence config to:
|
||||
- cadence = daily
|
||||
- log_dir = task-logs/daily/
|
||||
- branch_prefix = agents/daily/
|
||||
- prompt_dir = src/prompts/daily/
|
||||
|
||||
MUST 2: Run preflight to get the exclusion set:
|
||||
|
||||
node src/nostr-lock.mjs check --cadence daily
|
||||
|
||||
Use the `locked` array from the JSON output as the exclusion set.
|
||||
|
||||
MUST 3: Run these commands in this order:
|
||||
1) cat AGENTS.md
|
||||
2) ls -1 task-logs/daily/ | sort | tail -n 1
|
||||
3) Select next roster agent not in exclusion set
|
||||
4) Claim via Nostr lock:
|
||||
AGENT_PLATFORM=<platform> node src/nostr-lock.mjs lock --agent <agent-name> --cadence daily
|
||||
Exit 0 = lock acquired, proceed. Exit 3 = race lost, go back to step 2.
|
||||
5) Execute selected prompt from src/prompts/daily/
|
||||
6) Run repository checks (for example: npm run lint)
|
||||
7) Create `_completed.md` or `_failed.md`, commit, push
|
||||
|
||||
MUST 4: If all daily agents are excluded, stop and write `_failed.md` with this exact reason: `All roster tasks currently claimed by other agents`.
|
||||
```
|
||||
|
||||
## Weekly Scheduler Meta Prompt
|
||||
|
||||
```text
|
||||
You are the weekly agent scheduler for this repository.
|
||||
|
||||
Follow `src/prompts/scheduler-flow.md` exactly.
|
||||
|
||||
MUST 1: Set cadence config to:
|
||||
- cadence = weekly
|
||||
- log_dir = task-logs/weekly/
|
||||
- branch_prefix = agents/weekly/
|
||||
- prompt_dir = src/prompts/weekly/
|
||||
|
||||
MUST 2: Run preflight to get the exclusion set:
|
||||
|
||||
node src/nostr-lock.mjs check --cadence weekly
|
||||
|
||||
Use the `locked` array from the JSON output as the exclusion set.
|
||||
|
||||
MUST 3: Run these commands in this order:
|
||||
1) cat AGENTS.md
|
||||
2) ls -1 task-logs/weekly/ | sort | tail -n 1
|
||||
3) Select next roster agent not in exclusion set
|
||||
4) Claim via Nostr lock:
|
||||
AGENT_PLATFORM=<platform> node src/nostr-lock.mjs lock --agent <agent-name> --cadence weekly
|
||||
Exit 0 = lock acquired, proceed. Exit 3 = race lost, go back to step 2.
|
||||
5) Execute selected prompt from src/prompts/weekly/
|
||||
6) Run repository checks (for example: npm run lint)
|
||||
7) Create `_completed.md` or `_failed.md`, commit, push
|
||||
|
||||
MUST 4: If all weekly agents are excluded, stop and write `_failed.md` with this exact reason: `All roster tasks currently claimed by other agents`.
|
||||
```
|
||||
@@ -0,0 +1,22 @@
|
||||
# Daily Agent Scheduler Prompt (Generic)
|
||||
|
||||
Use `src/prompts/scheduler-flow.md` as the authoritative scheduler procedure.
|
||||
|
||||
## Daily Cadence Configuration
|
||||
|
||||
- `cadence`: `daily`
|
||||
- `log_dir`: `task-logs/daily/`
|
||||
- `branch_prefix`: `agents/daily/`
|
||||
- `prompt_dir`: `src/prompts/daily/`
|
||||
|
||||
## Example Daily Roster
|
||||
|
||||
| # | Agent Name | Prompt File |
|
||||
|---|------------|-------------|
|
||||
| 1 | documentation-agent | `documentation-agent.md` |
|
||||
| 2 | quality-agent | `quality-agent.md` |
|
||||
| 3 | security-agent | `security-agent.md` |
|
||||
| 4 | performance-agent | `performance-agent.md` |
|
||||
| 5 | refactor-agent | `refactor-agent.md` |
|
||||
|
||||
Replace this roster with your project-specific task agents.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Scheduler Flow (Single Source of Truth)
|
||||
|
||||
Use this document for all scheduler runs.
|
||||
|
||||
## Numbered MUST Procedure
|
||||
|
||||
1. Set cadence variables before any command:
|
||||
- `cadence` = `daily` or `weekly`
|
||||
- `log_dir` = `task-logs/<cadence>/`
|
||||
- `branch_prefix` = `agents/<cadence>/`
|
||||
- `prompt_dir` = `src/prompts/<cadence>/`
|
||||
|
||||
2. Run preflight to build the exclusion set:
|
||||
|
||||
```bash
|
||||
node src/nostr-lock.mjs check --cadence <cadence>
|
||||
```
|
||||
|
||||
3. Read policy file(s):
|
||||
|
||||
```bash
|
||||
cat AGENTS.md
|
||||
```
|
||||
|
||||
4. Find latest cadence log file, then choose next roster agent not in exclusion set:
|
||||
|
||||
```bash
|
||||
ls -1 <log_dir> | sort | tail -n 1
|
||||
```
|
||||
|
||||
5. If every roster agent is excluded, write a `_failed.md` log with:
|
||||
`All roster tasks currently claimed by other agents` and stop.
|
||||
|
||||
6. Claim selected agent:
|
||||
|
||||
```bash
|
||||
AGENT_PLATFORM=<platform> \
|
||||
node src/nostr-lock.mjs lock --agent <agent-name> --cadence <cadence>
|
||||
```
|
||||
|
||||
- Exit `0`: lock acquired, continue.
|
||||
- Exit `3`: race lost/already locked, return to step 2.
|
||||
- Exit `2`: relay error, write `_failed.md` with reason `Nostr relay error`, stop.
|
||||
|
||||
7. Execute `<prompt_dir>/<prompt-file>` end-to-end.
|
||||
|
||||
8. Create exactly one final status file (`_completed.md` or `_failed.md`), run repo checks, commit, and push.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Weekly Agent Scheduler Prompt (Generic)
|
||||
|
||||
Use `src/prompts/scheduler-flow.md` as the authoritative scheduler procedure.
|
||||
|
||||
## Weekly Cadence Configuration
|
||||
|
||||
- `cadence`: `weekly`
|
||||
- `log_dir`: `task-logs/weekly/`
|
||||
- `branch_prefix`: `agents/weekly/`
|
||||
- `prompt_dir`: `src/prompts/weekly/`
|
||||
|
||||
## Example Weekly Roster
|
||||
|
||||
| # | Agent Name | Prompt File |
|
||||
|---|------------|-------------|
|
||||
| 1 | bug-reproducer-agent | `bug-reproducer-agent.md` |
|
||||
| 2 | integration-agent | `integration-agent.md` |
|
||||
| 3 | release-agent | `release-agent.md` |
|
||||
| 4 | test-coverage-agent | `test-coverage-agent.md` |
|
||||
| 5 | weekly-synthesis-agent | `weekly-synthesis-agent.md` |
|
||||
|
||||
Replace this roster with your project-specific task agents.
|
||||
Reference in new issue
Block a user