diff --git a/memory-updates/2026-02-21T22-21-27Z__decompose-agent.md b/memory-updates/2026-02-21T22-21-27Z__decompose-agent.md new file mode 100644 index 0000000..398a253 --- /dev/null +++ b/memory-updates/2026-02-21T22-21-27Z__decompose-agent.md @@ -0,0 +1,12 @@ +# Memory Update — decompose-agent — 2026-02-21 + +## Key findings +- Extracted `cmdCheck`, `cmdList`, `cmdComplete` from `src/lib.mjs` into separate files. +- `scripts/check-file-size.mjs` does not have a `BASELINE` object to update, effectively a read-only report tool. + +## Patterns / reusable knowledge +- When decomposing `src/lib.mjs`, ensure dependencies (imports) are correctly moved or re-imported. +- `COMMAND_HANDLERS` in `src/lib.mjs` makes it easy to swap implementations. + +## Warnings / gotchas +- `scripts/check-file-size.mjs` --update flag does not update the file itself. diff --git a/src/cmd-check.mjs b/src/cmd-check.mjs new file mode 100644 index 0000000..12c29b5 --- /dev/null +++ b/src/cmd-check.mjs @@ -0,0 +1,115 @@ +import { + getRelays as _getRelays, + getNamespace as _getNamespace, + loadTorchConfig as _loadTorchConfig, +} from './torch-config.mjs'; +import { todayDateStr } from './utils.mjs'; +import { getRoster as _getRoster } from './roster.mjs'; +import { queryLocks as _queryLocks } from './lock-ops.mjs'; +import { getCompletedAgents } from './lock-utils.mjs'; +import path from 'node:path'; +import fs from 'node:fs/promises'; + +/** + * Checks the status of the repository locks for a given cadence. + * + * It aggregates data from three sources: + * 1. Configuration (paused agents) + * 2. Local logs (completed agents) + * 3. Nostr relays (current active locks) + * + * It outputs a JSON object describing the state (locked, available, excluded agents). + * + * @param {string} cadence - 'daily' or 'weekly' + * @param {Object} [deps] - Dependency injection + * @returns {Promise} - The check result object + */ +export async function cmdCheck(cadence, deps = {}) { + const { + getRelays = _getRelays, + getNamespace = _getNamespace, + loadTorchConfig = _loadTorchConfig, + queryLocks = _queryLocks, + getRoster = _getRoster, + getDateStr = todayDateStr, + log = console.log, + error = console.error, + logDir = 'task-logs', + ignoreLogs = false, + json = false, + jsonFile = null, + quiet = false, + } = deps; + + const relays = await getRelays(); + const namespace = await getNamespace(); + const dateStr = getDateStr(); + const config = await loadTorchConfig(); + const pausedAgents = config.scheduler.paused[cadence] || []; + + if (!quiet) { + error(`Checking locks: namespace=${namespace}, cadence=${cadence}, date=${dateStr}`); + error(`Relays: ${relays.join(', ')}`); + if (pausedAgents.length > 0) { + error(`Paused agents: ${pausedAgents.join(', ')}`); + } + } + + let completedAgents = new Set(); + if (!ignoreLogs) { + completedAgents = await getCompletedAgents(cadence, logDir, deps); + if (!quiet && completedAgents.size > 0) { + error(`Completed agents (logs): ${[...completedAgents].join(', ')}`); + } + } + + const locks = await queryLocks( + relays, + cadence, + dateStr, + namespace, + quiet ? { errorLogger: () => {}, healthLogger: () => {} } : {}, + ); + const lockedAgents = [...new Set(locks.map((l) => l.agent).filter(Boolean))]; + const roster = await getRoster(cadence); + const rosterSet = new Set(roster); + + const excludedAgentsSet = new Set([...lockedAgents, ...pausedAgents, ...completedAgents]); + const excludedAgents = [...excludedAgentsSet]; + const unknownLockedAgents = lockedAgents.filter((agent) => !rosterSet.has(agent)); + const available = roster.filter((a) => !excludedAgentsSet.has(a)); + + const result = { + namespace, + cadence, + date: dateStr, + locked: lockedAgents.sort(), + paused: pausedAgents.sort(), + completed: [...completedAgents].sort(), + excluded: excludedAgents.sort(), + available: available.sort(), + lockCount: locks.length, + unknownLockedAgents: unknownLockedAgents.sort(), + locks: locks.map((l) => ({ + agent: l.agent, + eventId: l.eventId, + createdAt: l.createdAtIso, + expiresAt: l.expiresAtIso, + platform: l.platform, + })), + }; + + const output = json ? JSON.stringify(result) : JSON.stringify(result, null, 2); + + if (jsonFile) { + const resolvedPath = path.resolve(process.cwd(), jsonFile); + await fs.mkdir(path.dirname(resolvedPath), { recursive: true }); + await fs.writeFile(resolvedPath, `${output}\n`, 'utf8'); + } + + if (json || !quiet) { + log(output); + } + + return result; +} diff --git a/src/cmd-complete.mjs b/src/cmd-complete.mjs new file mode 100644 index 0000000..728b5df --- /dev/null +++ b/src/cmd-complete.mjs @@ -0,0 +1,124 @@ +import { + getRelays as _getRelays, + getNamespace as _getNamespace, + getHashtag as _getHashtag, +} from './torch-config.mjs'; +import { + KIND_APP_DATA, + MS_PER_SECOND, +} from './constants.mjs'; +import { todayDateStr, nowUnix, detectPlatform } from './utils.mjs'; +import { queryLocks as _queryLocks, publishLock as _publishLock } from './lock-ops.mjs'; +import { ExitError } from './errors.mjs'; +import { generateSecretKey as _generateSecretKey, getPublicKey as _getPublicKey, finalizeEvent as _finalizeEvent } from 'nostr-tools/pure'; + +/** + * Marks a task as permanently completed by publishing a new lock event with + * `status: 'completed'` and no expiration. + * + * This function: + * 1. Verifies that the agent currently holds a valid lock. + * 2. Publishes a replacement event that preserves the original `startedAt` time. + * + * @param {string} agent - Agent name + * @param {string} cadence - 'daily' or 'weekly' + * @param {boolean} [dryRun=false] - If true, skips publishing + * @param {Object} [deps] - Dependency injection + * @returns {Promise<{status: string, eventId: string}>} + * @throws {ExitError} If no active lock exists for the agent + */ +export async function cmdComplete(agent, cadence, optionsOrDryRun = false, deps = {}) { + const options = typeof optionsOrDryRun === 'object' ? optionsOrDryRun : { dryRun: !!optionsOrDryRun }; + const { dryRun = false, platform = null, model = null } = options; + + const { + getRelays = _getRelays, + getNamespace = _getNamespace, + getHashtag = _getHashtag, + queryLocks = _queryLocks, + publishLock = _publishLock, + generateSecretKey = _generateSecretKey, + getPublicKey = _getPublicKey, + finalizeEvent = _finalizeEvent, + getDateStr = todayDateStr, + log = console.log, + error = console.error + } = deps; + + const relays = await getRelays(); + const namespace = await getNamespace(); + const hashtag = await getHashtag(); + const dateStr = getDateStr(); + const now = nowUnix(); + + error(`Completing task: namespace=${namespace}, agent=${agent}, cadence=${cadence}, date=${dateStr}`); + error(`Relays: ${relays.join(', ')}`); + + // 1. Find existing lock + const locks = await queryLocks(relays, cadence, dateStr, namespace); + const myLock = locks.find((l) => l.agent === agent); + + if (!myLock) { + error(`ERROR: No active lock found for agent "${agent}" on ${dateStr}.`); + error(`Cannot complete a task that is not locked or has already expired.`); + throw new ExitError(1, 'No active lock found'); + } + + if (myLock.status === 'completed') { + error(`Task is already marked as completed (event ${myLock.eventId}).`); + log('LOCK_STATUS=completed'); + return { status: 'completed', eventId: myLock.eventId }; + } + + // 2. Build completion event + const startedAtIso = myLock.createdAtIso; + + error('Step 1: Generating completion event...'); + const sk = generateSecretKey(); + const pk = getPublicKey(sk); + + const event = finalizeEvent( + { + kind: KIND_APP_DATA, + created_at: now, + tags: [ + ['d', `${namespace}-lock/${cadence}/${agent}/${dateStr}`], + ['t', hashtag], + ['t', `${namespace}-lock-${cadence}`], + ['t', `${namespace}-lock-${cadence}-${dateStr}`], + // No expiration tag -> permanent + ], + content: JSON.stringify({ + agent, + cadence, + status: 'completed', + namespace, + date: dateStr, + platform: platform || process.env.AGENT_PLATFORM || detectPlatform() || 'unknown', + model: model || process.env.AGENT_MODEL || 'unknown', + startedAt: startedAtIso, + completedAt: new Date(now * MS_PER_SECOND).toISOString(), + }), + }, + sk, + ); + + error(` Event ID: ${event.id}`); + + if (dryRun) { + error('Step 2: [DRY RUN] Skipping publish — event built but not sent'); + } else { + error('Step 2: Publishing completion event...'); + await publishLock(relays, event); + error(' Published successfully.'); + } + + log('LOCK_STATUS=completed'); + log(`LOCK_EVENT_ID=${event.id}`); + log(`LOCK_PUBKEY=${pk}`); + log(`LOCK_AGENT=${agent}`); + log(`LOCK_CADENCE=${cadence}`); + log(`LOCK_DATE=${dateStr}`); + + return { status: 'completed', eventId: event.id }; +} diff --git a/src/cmd-list.mjs b/src/cmd-list.mjs index 8e36812..ce25df2 100644 --- a/src/cmd-list.mjs +++ b/src/cmd-list.mjs @@ -2,26 +2,36 @@ import { getRelays as _getRelays, getNamespace as _getNamespace, } from './torch-config.mjs'; +import { + VALID_CADENCES, +} from './constants.mjs'; +import { todayDateStr, nowUnix } from './utils.mjs'; import { getRoster as _getRoster } from './roster.mjs'; import { queryLocks as _queryLocks } from './lock-ops.mjs'; -import { todayDateStr as _todayDateStr, nowUnix as _nowUnix } from './utils.mjs'; +/** + * Lists all active locks for the specified cadence (or all cadences if null). + * It prints a formatted table to stdout with lock age, TTL, and event ID. + * + * @param {string|null} cadence - Filter by cadence ('daily', 'weekly') or null for all + * @param {Object} [deps] - Dependency injection + * @returns {Promise} + */ export async function cmdList(cadence, deps = {}) { const { - getRelays = _getRelays, - getNamespace = _getNamespace, - getRoster = _getRoster, - queryLocks = _queryLocks, - todayDateStr = _todayDateStr, - nowUnix = _nowUnix, - log = console.log, - error = console.error, + getRelays = _getRelays, + getNamespace = _getNamespace, + queryLocks = _queryLocks, + getRoster = _getRoster, + getDateStr = todayDateStr, + log = console.log, + error = console.error } = deps; const relays = await getRelays(); const namespace = await getNamespace(); - const dateStr = todayDateStr(); - const cadences = cadence ? [cadence] : ['daily', 'weekly']; + const dateStr = getDateStr(); + const cadences = cadence ? [cadence] : [...VALID_CADENCES]; error(`Listing active locks: namespace=${namespace}, cadences=${cadences.join(', ')}`); @@ -46,13 +56,19 @@ export async function cmdList(cadence, deps = {}) { 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) : '?'; + + let remainMin = '?'; + if (lock.status === 'completed') { + remainMin = 'done'; + } else if (lock.expiresAt) { + const remaining = lock.expiresAt - nowUnix(); + remainMin = Math.round(remaining / 60); + } log( ` ${(lock.agent ?? 'unknown').padEnd(30)} ` + `age: ${String(ageMin).padStart(4)}m ` + - `ttl: ${String(remainMin).padStart(4)}m ` + + `ttl: ${String(remainMin).padStart(4)} ` + `platform: ${lock.platform ?? '?'} ` + `event: ${lock.eventId?.slice(0, 12)}...`, ); diff --git a/src/context/CONTEXT_2026-02-21T22-21-27Z.md b/src/context/CONTEXT_2026-02-21T22-21-27Z.md new file mode 100644 index 0000000..5792978 --- /dev/null +++ b/src/context/CONTEXT_2026-02-21T22-21-27Z.md @@ -0,0 +1,31 @@ +# Context: Decompose Agent Run 2026-02-21T22-21-27Z + +- Agent: decompose-agent +- Cadence: daily +- Session: 2026-02-21T22-21-27Z +- Run-Start: 2026-02-21T22-21-27Z +- Prompt: src/prompts/daily/decompose-agent.md + +## Goal +Decompose `src/lib.mjs` into smaller modules to reduce size by at least 200 lines. + +## Scope +Decomposition of `src/lib.mjs` by extracting command implementations. + +## Constraints +- Do not change behavior. +- Reduce size by at least 200 lines. +- Preserve existing functionality. + +## Selection +- File: `src/lib.mjs` +- Original Size: 730 lines +- Reason: Largest grandfathered file not recently refactored (based on manual check). +- Commit SHA: a6c06ce173fbbff9170476e4b97c1bac4b30cb10 +- Node/npm: v22.22.0 / 11.7.0 + +## Plan +Extract cohesive blocks: +1. `cmdCheck` +2. `cmdList` +3. `cmdComplete` diff --git a/src/decisions/DECISIONS_2026-02-21T22-21-27Z.md b/src/decisions/DECISIONS_2026-02-21T22-21-27Z.md new file mode 100644 index 0000000..2804a4f --- /dev/null +++ b/src/decisions/DECISIONS_2026-02-21T22-21-27Z.md @@ -0,0 +1,22 @@ +# Decisions: Decompose src/lib.mjs + +- Agent: decompose-agent +- Cadence: daily +- Session: 2026-02-21T22-21-27Z +- Run-Start: 2026-02-21T22-21-27Z +- Prompt: src/prompts/daily/decompose-agent.md + +## Decision +Extract `cmdCheck`, `cmdList`, and `cmdComplete` into separate modules. + +## Rationale +These are distinct, cohesive command implementations that can be cleanly separated from the main entry point to reduce file size and improve maintainability. `src/lib.mjs` was over 700 lines. + +## Naming +- `src/cmd-check.mjs`: Specific to check command. +- `src/cmd-list.mjs`: Specific to list command. +- `src/cmd-complete.mjs`: Specific to complete command. + +## Baseline Update +- Reduced `src/lib.mjs` from 730 lines to 438 lines (292 lines reduced). +- `scripts/check-file-size.mjs` does not support programmatic baseline updates or contain a `BASELINE` object, so manual update of the script was not possible/needed. diff --git a/src/lib.mjs b/src/lib.mjs index ddb87e1..280f439 100644 --- a/src/lib.mjs +++ b/src/lib.mjs @@ -7,7 +7,6 @@ import { generateSecretKey as _generateSecretKey, getPublicKey as _getPublicKey, import { useWebSocketImplementation } from 'nostr-tools/relay'; import WebSocket from 'ws'; import { - loadTorchConfig as _loadTorchConfig, getRelays as _getRelays, getNamespace as _getNamespace, getTtl as _getTtl, @@ -40,118 +39,16 @@ import fs from 'node:fs/promises'; import path from 'node:path'; import { createHash } from 'node:crypto'; import { execSync } from 'node:child_process'; -import { getCompletedAgents } from './lock-utils.mjs'; import { cmdProposal } from './cmd-proposal.mjs'; import { cmdRollback } from './cmd-rollback.mjs'; +import { cmdCheck } from './cmd-check.mjs'; +import { cmdList } from './cmd-list.mjs'; +import { cmdComplete } from './cmd-complete.mjs'; useWebSocketImplementation(WebSocket); // Re-export for backward compatibility/library usage -export { parseLockEvent, cmdDashboard, _queryLocks as queryLocks, _publishLock as publishLock }; - -/** - * Checks the status of the repository locks for a given cadence. - * - * It aggregates data from three sources: - * 1. Configuration (paused agents) - * 2. Local logs (completed agents) - * 3. Nostr relays (current active locks) - * - * It outputs a JSON object describing the state (locked, available, excluded agents). - * - * @param {string} cadence - 'daily' or 'weekly' - * @param {Object} [deps] - Dependency injection - * @returns {Promise} - The check result object - */ -export async function cmdCheck(cadence, deps = {}) { - const { - getRelays = _getRelays, - getNamespace = _getNamespace, - loadTorchConfig = _loadTorchConfig, - queryLocks = _queryLocks, - getRoster = _getRoster, - getDateStr = todayDateStr, - log = console.log, - error = console.error, - logDir = 'task-logs', - ignoreLogs = false, - json = false, - jsonFile = null, - quiet = false, - } = deps; - - const relays = await getRelays(); - const namespace = await getNamespace(); - const dateStr = getDateStr(); - const config = await loadTorchConfig(); - const pausedAgents = config.scheduler.paused[cadence] || []; - - if (!quiet) { - error(`Checking locks: namespace=${namespace}, cadence=${cadence}, date=${dateStr}`); - error(`Relays: ${relays.join(', ')}`); - if (pausedAgents.length > 0) { - error(`Paused agents: ${pausedAgents.join(', ')}`); - } - } - - let completedAgents = new Set(); - if (!ignoreLogs) { - completedAgents = await getCompletedAgents(cadence, logDir, deps); - if (!quiet && completedAgents.size > 0) { - error(`Completed agents (logs): ${[...completedAgents].join(', ')}`); - } - } - - const locks = await queryLocks( - relays, - cadence, - dateStr, - namespace, - quiet ? { errorLogger: () => {}, healthLogger: () => {} } : {}, - ); - const lockedAgents = [...new Set(locks.map((l) => l.agent).filter(Boolean))]; - const roster = await getRoster(cadence); - const rosterSet = new Set(roster); - - const excludedAgentsSet = new Set([...lockedAgents, ...pausedAgents, ...completedAgents]); - const excludedAgents = [...excludedAgentsSet]; - const unknownLockedAgents = lockedAgents.filter((agent) => !rosterSet.has(agent)); - const available = roster.filter((a) => !excludedAgentsSet.has(a)); - - const result = { - namespace, - cadence, - date: dateStr, - locked: lockedAgents.sort(), - paused: pausedAgents.sort(), - completed: [...completedAgents].sort(), - excluded: excludedAgents.sort(), - available: available.sort(), - lockCount: locks.length, - unknownLockedAgents: unknownLockedAgents.sort(), - locks: locks.map((l) => ({ - agent: l.agent, - eventId: l.eventId, - createdAt: l.createdAtIso, - expiresAt: l.expiresAtIso, - platform: l.platform, - })), - }; - - const output = json ? JSON.stringify(result) : JSON.stringify(result, null, 2); - - if (jsonFile) { - const resolvedPath = path.resolve(process.cwd(), jsonFile); - await fs.mkdir(path.dirname(resolvedPath), { recursive: true }); - await fs.writeFile(resolvedPath, `${output}\n`, 'utf8'); - } - - if (json || !quiet) { - log(output); - } - - return result; -} +export { parseLockEvent, cmdDashboard, _queryLocks as queryLocks, _publishLock as publishLock, cmdCheck, cmdList, cmdComplete }; /** * Attempts to acquire an exclusive lock for an agent on the specified cadence. @@ -339,197 +236,6 @@ export async function cmdLock(agent, cadence, optionsOrDryRun = false, deps = {} return { status: 'ok', eventId: event.id }; } -/** - * Lists all active locks for the specified cadence (or all cadences if null). - * It prints a formatted table to stdout with lock age, TTL, and event ID. - * - * @param {string|null} cadence - Filter by cadence ('daily', 'weekly') or null for all - * @param {Object} [deps] - Dependency injection - * @returns {Promise} - */ -export async function cmdList(cadence, deps = {}) { - const { - getRelays = _getRelays, - getNamespace = _getNamespace, - queryLocks = _queryLocks, - getRoster = _getRoster, - getDateStr = todayDateStr, - log = console.log, - error = console.error - } = deps; - - const relays = await getRelays(); - const namespace = await getNamespace(); - const dateStr = getDateStr(); - const cadences = cadence ? [cadence] : [...VALID_CADENCES]; - - error(`Listing active locks: namespace=${namespace}, cadences=${cadences.join(', ')}`); - - const results = await Promise.all( - cadences.map(async (c) => { - const locks = await queryLocks(relays, c, dateStr, namespace); - return { c, locks }; - }), - ); - - for (const { c, locks } of results) { - log(`\n${'='.repeat(72)}`); - log(`Active ${namespace} ${c} locks (${dateStr})`); - log('='.repeat(72)); - - if (locks.length === 0) { - 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); - - let remainMin = '?'; - if (lock.status === 'completed') { - remainMin = 'done'; - } else if (lock.expiresAt) { - const remaining = lock.expiresAt - nowUnix(); - remainMin = Math.round(remaining / 60); - } - - log( - ` ${(lock.agent ?? 'unknown').padEnd(30)} ` + - `age: ${String(ageMin).padStart(4)}m ` + - `ttl: ${String(remainMin).padStart(4)} ` + - `platform: ${lock.platform ?? '?'} ` + - `event: ${lock.eventId?.slice(0, 12)}...`, - ); - } - - const roster = await getRoster(c); - const rosterSet = new Set(roster); - const lockedAgents = new Set(locks.map((l) => l.agent).filter(Boolean)); - const unknownLockedAgents = [...lockedAgents].filter((agent) => !rosterSet.has(agent)); - const available = roster.filter((a) => !lockedAgents.has(a)); - - if (unknownLockedAgents.length > 0) { - log(` Warning: lock events found with non-roster agent names: ${unknownLockedAgents.join(', ')}`); - } - - log(`\n Locked: ${lockedAgents.size}/${roster.length}`); - log(` Available: ${available.join(', ') || '(none)'}`); - } -} - -/** - * Marks a task as permanently completed by publishing a new lock event with - * `status: 'completed'` and no expiration. - * - * This function: - * 1. Verifies that the agent currently holds a valid lock. - * 2. Publishes a replacement event that preserves the original `startedAt` time. - * - * @param {string} agent - Agent name - * @param {string} cadence - 'daily' or 'weekly' - * @param {boolean} [dryRun=false] - If true, skips publishing - * @param {Object} [deps] - Dependency injection - * @returns {Promise<{status: string, eventId: string}>} - * @throws {ExitError} If no active lock exists for the agent - */ -export async function cmdComplete(agent, cadence, optionsOrDryRun = false, deps = {}) { - const options = typeof optionsOrDryRun === 'object' ? optionsOrDryRun : { dryRun: !!optionsOrDryRun }; - const { dryRun = false, platform = null, model = null } = options; - - const { - getRelays = _getRelays, - getNamespace = _getNamespace, - getHashtag = _getHashtag, - queryLocks = _queryLocks, - publishLock = _publishLock, - generateSecretKey = _generateSecretKey, - getPublicKey = _getPublicKey, - finalizeEvent = _finalizeEvent, - getDateStr = todayDateStr, - log = console.log, - error = console.error - } = deps; - - const relays = await getRelays(); - const namespace = await getNamespace(); - const hashtag = await getHashtag(); - const dateStr = getDateStr(); - const now = nowUnix(); - - error(`Completing task: namespace=${namespace}, agent=${agent}, cadence=${cadence}, date=${dateStr}`); - error(`Relays: ${relays.join(', ')}`); - - // 1. Find existing lock - const locks = await queryLocks(relays, cadence, dateStr, namespace); - const myLock = locks.find((l) => l.agent === agent); - - if (!myLock) { - error(`ERROR: No active lock found for agent "${agent}" on ${dateStr}.`); - error(`Cannot complete a task that is not locked or has already expired.`); - throw new ExitError(1, 'No active lock found'); - } - - if (myLock.status === 'completed') { - error(`Task is already marked as completed (event ${myLock.eventId}).`); - log('LOCK_STATUS=completed'); - return { status: 'completed', eventId: myLock.eventId }; - } - - // 2. Build completion event - const startedAtIso = myLock.createdAtIso; - - error('Step 1: Generating completion event...'); - const sk = generateSecretKey(); - const pk = getPublicKey(sk); - - const event = finalizeEvent( - { - kind: KIND_APP_DATA, - created_at: now, - tags: [ - ['d', `${namespace}-lock/${cadence}/${agent}/${dateStr}`], - ['t', hashtag], - ['t', `${namespace}-lock-${cadence}`], - ['t', `${namespace}-lock-${cadence}-${dateStr}`], - // No expiration tag -> permanent - ], - content: JSON.stringify({ - agent, - cadence, - status: 'completed', - namespace, - date: dateStr, - platform: platform || process.env.AGENT_PLATFORM || detectPlatform() || 'unknown', - model: model || process.env.AGENT_MODEL || 'unknown', - startedAt: startedAtIso, - completedAt: new Date(now * MS_PER_SECOND).toISOString(), - }), - }, - sk, - ); - - error(` Event ID: ${event.id}`); - - if (dryRun) { - error('Step 2: [DRY RUN] Skipping publish — event built but not sent'); - } else { - error('Step 2: Publishing completion event...'); - await publishLock(relays, event); - error(' Published successfully.'); - } - - log('LOCK_STATUS=completed'); - log(`LOCK_EVENT_ID=${event.id}`); - log(`LOCK_PUBKEY=${pk}`); - log(`LOCK_AGENT=${agent}`); - log(`LOCK_CADENCE=${cadence}`); - log(`LOCK_DATE=${dateStr}`); - - return { status: 'completed', eventId: event.id }; -} - const COMMAND_HANDLERS = { check: async (args) => { if (!args.cadence || !VALID_CADENCES.has(args.cadence)) { diff --git a/src/test_logs/TEST_LOG_2026-02-21T22-21-27Z.md b/src/test_logs/TEST_LOG_2026-02-21T22-21-27Z.md new file mode 100644 index 0000000..2cf23a9 --- /dev/null +++ b/src/test_logs/TEST_LOG_2026-02-21T22-21-27Z.md @@ -0,0 +1,15 @@ +# Test Log: Decompose Agent Run 2026-02-21T22-21-27Z + +- Agent: decompose-agent +- Cadence: daily +- Session: 2026-02-21T22-21-27Z +- Run-Start: 2026-02-21T22-21-27Z +- Prompt: src/prompts/daily/decompose-agent.md + +## Lint +- Command: npm run lint +- Result: Passed + +## Unit Tests +- Command: npm test +- Result: Passed (All tests passed) diff --git a/src/todo/TODO_2026-02-21T22-21-27Z.md b/src/todo/TODO_2026-02-21T22-21-27Z.md new file mode 100644 index 0000000..5a87ae6 --- /dev/null +++ b/src/todo/TODO_2026-02-21T22-21-27Z.md @@ -0,0 +1,15 @@ +# TODO: Decompose src/lib.mjs + +- Agent: decompose-agent +- Cadence: daily +- Session: 2026-02-21T22-21-27Z +- Run-Start: 2026-02-21T22-21-27Z +- Prompt: src/prompts/daily/decompose-agent.md + +## Pending Tasks +- [x] Extract `cmdCheck` logic to `src/cmd-check.mjs` +- [x] Extract `cmdList` logic to `src/cmd-list.mjs` +- [x] Extract `cmdComplete` logic to `src/cmd-complete.mjs` +- [x] Update `src/lib.mjs` imports +- [x] Verify `npm run lint` +- [x] Verify `npm test` diff --git a/task-logs/daily/2026-02-21T22-21-27Z__decompose-agent__completed.md b/task-logs/daily/2026-02-21T22-21-27Z__decompose-agent__completed.md new file mode 100644 index 0000000..c86c1f1 --- /dev/null +++ b/task-logs/daily/2026-02-21T22-21-27Z__decompose-agent__completed.md @@ -0,0 +1,11 @@ +--- +agent: decompose-agent +cadence: daily +session: 2026-02-21T22-21-27Z +run-start: 2026-02-21T22-21-27Z +prompt: src/prompts/daily/decompose-agent.md +platform: linux +--- + +Decomposed `src/lib.mjs` into `src/cmd-check.mjs`, `src/cmd-list.mjs`, and `src/cmd-complete.mjs`. +Reduced file size by ~292 lines. diff --git a/task-logs/relay-health/daily.jsonl b/task-logs/relay-health/daily.jsonl index 756d54a..2b1a872 100644 --- a/task-logs/relay-health/daily.jsonl +++ b/task-logs/relay-health/daily.jsonl @@ -1,2 +1,3 @@ {"ok":true,"cadence":"daily","namespace":"torch","timestamp":"2026-02-20T01:13:27.775Z","relays":["wss://relay.damus.io","wss://nos.lol","wss://relay.primal.net"],"relayResults":[{"relay":"wss://relay.damus.io","websocketReachable":true,"publishReadOk":true,"healthy":true,"checks":{"websocket":{"ok":true,"latencyMs":186},"publishRead":{"ok":true,"eventId":"7d217815c782e1dac6557a8b616f30713820872c3e2f13e49c82fc6f787269e2"}}},{"relay":"wss://nos.lol","websocketReachable":true,"publishReadOk":true,"healthy":true,"checks":{"websocket":{"ok":true,"latencyMs":358},"publishRead":{"ok":true,"eventId":"ce6e9fe40d4365b0a36ee90d15aad91fe28ddafc25b1b2d2887bbd54a5e6cf90"}}},{"relay":"wss://relay.primal.net","websocketReachable":true,"publishReadOk":true,"healthy":true,"checks":{"websocket":{"ok":true,"latencyMs":520},"publishRead":{"ok":true,"eventId":"0bc4ad0f2ed9d580d0147b9af847dffd3009ec9419e516c0105d29837c9bcd71"}}}],"summary":{"totalRelays":3,"healthyRelays":3,"unhealthyRelays":0,"allRelaysUnhealthy":false},"incidentSignal":null,"alerts":[],"alertStats":{"windowMinutes":60,"sampleCount":1,"successRate":1,"allDownDurationMinutes":0},"historyPath":"/app/task-logs/relay-health/daily.jsonl"} {"ok":false,"cadence":"daily","namespace":"torch","timestamp":"2026-02-20T04:26:35.357Z","relays":["wss://relay.damus.io","wss://nos.lol","wss://relay.primal.net"],"relayResults":[{"relay":"wss://relay.damus.io","websocketReachable":false,"publishReadOk":false,"healthy":false,"checks":{"websocket":{"ok":false,"error":"getaddrinfo ENOTFOUND relay.damus.io"}}},{"relay":"wss://nos.lol","websocketReachable":false,"publishReadOk":false,"healthy":false,"checks":{"websocket":{"ok":false,"error":"getaddrinfo ENOTFOUND nos.lol"}}},{"relay":"wss://relay.primal.net","websocketReachable":false,"publishReadOk":false,"healthy":false,"checks":{"websocket":{"ok":false,"error":"getaddrinfo ENOTFOUND relay.primal.net"}}}],"summary":{"totalRelays":3,"healthyRelays":0,"unhealthyRelays":3,"allRelaysUnhealthy":true},"incidentSignal":{"id":"relay-health-daily-1771561595358","reason":"All relays unhealthy before lock acquisition","severity":"critical","suggestedAction":"Defer scheduler run and investigate relay availability before retrying lock acquisition."},"alerts":[{"type":"all_relays_down_duration","thresholdMinutes":10,"actualMinutes":193,"severity":"critical"},{"type":"relay_success_rate_below_threshold","threshold":0.7,"actual":0,"windowMinutes":60,"severity":"warning"}],"alertStats":{"windowMinutes":60,"sampleCount":1,"successRate":0,"allDownDurationMinutes":193},"historyPath":"/home/user/Documents/GitHub/TORCH/task-logs/relay-health/daily.jsonl"} +{"ok":true,"cadence":"daily","namespace":"torch","timestamp":"2026-02-21T22:20:34.054Z","relays":["wss://relay.damus.io","wss://nos.lol","wss://relay.primal.net"],"relayResults":[{"relay":"wss://relay.damus.io","websocketReachable":true,"publishReadOk":true,"healthy":true,"checks":{"websocket":{"ok":true,"latencyMs":183},"publishRead":{"ok":true,"eventId":"eed1c45fa6fdd7f55e36b06bedfa48709c574f3dcd6bce298c4b8e8cdc6de716"}}},{"relay":"wss://nos.lol","websocketReachable":true,"publishReadOk":true,"healthy":true,"checks":{"websocket":{"ok":true,"latencyMs":365},"publishRead":{"ok":true,"eventId":"6a9e536a302871f8ab6648ee7f90952c110582997d04e2cff6e0c39b53face3e"}}},{"relay":"wss://relay.primal.net","websocketReachable":true,"publishReadOk":true,"healthy":true,"checks":{"websocket":{"ok":true,"latencyMs":484},"publishRead":{"ok":true,"eventId":"f03863e5a2e3bbecc9102745bf9a96b069d3f9d03bf0f47a4c3c81cd57400478"}}}],"summary":{"totalRelays":3,"healthyRelays":3,"unhealthyRelays":0,"allRelaysUnhealthy":false},"incidentSignal":null,"alerts":[],"alertStats":{"windowMinutes":60,"sampleCount":1,"successRate":1,"allDownDurationMinutes":0},"historyPath":"/app/task-logs/relay-health/daily.jsonl"}