diff --git a/docs/lib-overview.md b/docs/lib-overview.md new file mode 100644 index 0000000..fbce7ce --- /dev/null +++ b/docs/lib-overview.md @@ -0,0 +1,58 @@ +# src/lib.mjs: Core Logic Overview + +## Summary +`src/lib.mjs` is the orchestration engine for Torch. It implements the high-level commands (`check`, `lock`, `list`, `complete`) that coordinate between local configuration, the file system (task logs), and Nostr relays (distributed locks). + +It is designed to be: +- **Stateless**: No persistent local database; relies on Nostr events and flat files. +- **Race-Condition Safe**: Uses a commit-wait-reveal pattern to handle concurrent agent starts. +- **Dependency Injected**: Nearly all external side-effects (relays, time, FS) can be mocked. + +## Public API + +### `cmdCheck(cadence, deps)` +Aggregates the current state of the world for a given cadence. +- **Inputs**: Cadence ('daily'/'weekly'). +- **Outputs**: JSON object with lists of `locked`, `completed`, `paused`, and `available` agents. +- **Side Effects**: Queries relays, reads `task-logs/`, reads `torch-config.json`. + +### `cmdLock(agent, cadence, dryRun, deps)` +Attempts to claim a task. +- **Returns**: `{ status: 'ok', eventId: '...' }` +- **Throws**: `ExitError(3)` if the lock is denied (already active, completed, or race lost). + +### `cmdComplete(agent, cadence, dryRun, deps)` +Finalizes a task. +- **Action**: Publishes a permanent event (no expiration) with `status: 'completed'`. +- **Constraint**: Requires an existing active lock for the agent. + +### `cmdList(cadence, deps)` +Displays a CLI-friendly table of active locks. + +## Locking Flow (Sequence) + +1. **Check**: Query relays for any existing valid lock for `(agent, cadence, date)`. + - If found and `status=completed`: **Abort** (Task done). + - If found and `status=started` (and not expired): **Abort** (Agent busy). +2. **Generate**: Create a new ephemeral Nostr keypair. +3. **Build**: Create a signed event (Kind 30078) with: + - Tags: `d` (identifier), `expiration`, `t` (search tags). + - Content: JSON payload with metadata. +4. **Publish**: Send to all configured relays. +5. **Race Check**: + - Wait `RACE_CHECK_DELAY_MS` (default 2000ms). + - Query relays again. + - If another valid event exists with an *earlier* `created_at`: **Abort** (Race lost). + - Otherwise: **Success**. + +## Key Invariants + +- **Uniqueness**: Only one valid lock per (agent, cadence, date) should exist at a time. +- **Expiration**: All locks must have an `expiration` tag (NIP-40) to prevent zombies, except completion events. +- **Completion**: A completion event is permanent (no expiration) and effectively "freezes" the slot for that date. + +## When to Change + +- **Protocol Upgrades**: If changing the Nostr event structure or tags. +- **Concurrency Tuning**: Adjusting `RACE_CHECK_DELAY_MS` or retry logic. +- **New Commands**: Adding new lifecycle states (e.g., `failed` or `paused` via CLI). diff --git a/src/context/CONTEXT_2026-02-14T21-30-00Z.md b/src/context/CONTEXT_2026-02-14T21-30-00Z.md new file mode 100644 index 0000000..0f37987 --- /dev/null +++ b/src/context/CONTEXT_2026-02-14T21-30-00Z.md @@ -0,0 +1,9 @@ +# Context: 2026-02-14T21:30:00Z - docs-code-investigator + +## Plan +1. **Target**: `src/lib.mjs` (Core logic, ~700 LOC). +2. **Goal**: Add JSDoc and flow comments to clarify locking, checking, and completion logic. +3. **Rationale**: This file orchestrates the entire Torch system but lacks detailed documentation for its exported functions. +4. **Artifacts**: + * Inline JSDoc in `src/lib.mjs`. + * `docs/lib-overview.md` for high-level documentation. diff --git a/src/lib.mjs b/src/lib.mjs index 38dd2e4..b671af9 100644 --- a/src/lib.mjs +++ b/src/lib.mjs @@ -40,6 +40,15 @@ useWebSocketImplementation(WebSocket); // Re-export for backward compatibility/library usage export { parseLockEvent, cmdDashboard, _queryLocks as queryLocks, _publishLock as publishLock }; +/** + * Scans the local log directory to identify agents that have already completed their task + * for the current period (daily or weekly). + * + * @param {string} cadence - 'daily' or 'weekly' + * @param {string} logDir - Path to the log directory (e.g., 'task-logs') + * @param {Object} deps - Dependency injection for testing + * @returns {Promise>} - Set of agent names that have completed their task + */ async function getCompletedAgents(cadence, logDir, deps) { const { readdir = fs.readdir, getDateStr = todayDateStr, getIsoWeek = getIsoWeekStr } = deps; const completed = new Set(); @@ -77,6 +86,20 @@ async function getCompletedAgents(cadence, logDir, deps) { return completed; } +/** + * 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, @@ -145,6 +168,23 @@ export async function cmdCheck(cadence, deps = {}) { return result; } +/** + * Attempts to acquire an exclusive lock for an agent on the specified cadence. + * + * Algorithm: + * 1. Validate agent against the roster. + * 2. Query relays for existing valid locks (checking for conflicts). + * 3. Generate a new ephemeral keypair and build a lock event (kind 30078). + * 4. Publish the lock event to relays. + * 5. Wait for propagation (raceCheckDelayMs) and re-query to confirm no earlier lock won the race. + * + * @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 lock is denied (already locked, completed, or race lost) + */ export async function cmdLock(agent, cadence, dryRun = false, deps = {}) { const { getRelays = _getRelays, @@ -278,6 +318,14 @@ export async function cmdLock(agent, cadence, dryRun = 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, @@ -350,6 +398,21 @@ export async function cmdList(cadence, deps = {}) { } } +/** + * 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, dryRun = false, deps = {}) { const { getRelays = _getRelays, @@ -440,6 +503,12 @@ export async function cmdComplete(agent, cadence, dryRun = false, deps = {}) { return { status: 'completed', eventId: event.id }; } +/** + * Parses command-line arguments into a structured object. + * + * @param {string[]} argv - Arguments from process.argv.slice(2) + * @returns {Object} - Parsed arguments (command, flags, values) + */ function parseArgs(argv) { const args = { command: null, @@ -568,6 +637,12 @@ Exit codes: 3 Lock denied (already locked or race lost)`); } +/** + * Main entry point for the torch-lock CLI. + * Dispatches to specific commands (check, lock, complete, etc.) based on argv. + * + * @param {string[]} argv - Arguments from process.argv.slice(2) + */ export async function main(argv) { try { const args = parseArgs(argv); diff --git a/task-logs/daily/2026-02-14T21-30-00Z__docs-code-investigator__completed.md b/task-logs/daily/2026-02-14T21-30-00Z__docs-code-investigator__completed.md new file mode 100644 index 0000000..5013fcc --- /dev/null +++ b/task-logs/daily/2026-02-14T21-30-00Z__docs-code-investigator__completed.md @@ -0,0 +1,15 @@ +--- +agent: docs-code-investigator +status: completed +created_at: 2026-02-14T21:30:00Z +--- +# docs-code-investigator (daily) - 2026-02-14 + +## Task +Document `src/lib.mjs`. + +## Outcome +- Added JSDoc for exported functions (`cmdCheck`, `cmdLock`, `cmdList`, `cmdComplete`). +- Added flow comments for complex logic (`cmdLock`, `cmdComplete`). +- Created `docs/lib-overview.md`. +- Verified with `npm test` and `npm run lint`.