mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
Merge branch 'main' into refactor/lock-ops-reduction-3810095324988153600
This commit is contained in:
41 files changed
+7794
-241
No files matched your search
@@ -31,7 +31,7 @@ This file tracks external protocol specifications and libraries used in this rep
|
||||
|
||||
| Library | Version | Usage | Risks |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `nostr-tools` | `2.19.4` | Core Nostr primitives (keys, events, pool). | Supply chain risk. Pinned version recommended. |
|
||||
| `nostr-tools` | `2.23.1` | Core Nostr primitives (keys, events, pool). | Supply chain risk. Pinned version recommended. |
|
||||
| `ws` | `8.19.0` | WebSocket client for Node.js. | Standard library for Node.js websocket support. |
|
||||
|
||||
## Implementation details
|
||||
|
||||
@@ -56,6 +56,9 @@ AGENT_PLATFORM=codex npx --no-install torch-lock complete --agent docs-agent --c
|
||||
# Convenience script for the above
|
||||
npm run lock:complete -- --agent docs-agent --cadence daily
|
||||
|
||||
# Generic usage for lock completion
|
||||
npm run lock:complete -- --agent <agent> --cadence <cadence>
|
||||
|
||||
# Initialize TORCH in a new repository
|
||||
npx --no-install torch-lock init
|
||||
|
||||
@@ -247,14 +250,21 @@ If you are developing `torch-lock` itself:
|
||||
- `npm run lock:check:daily`
|
||||
- `npm run lock:check:weekly`
|
||||
- `npm run lock:list`
|
||||
- `npm run lock:lock` (manually acquire a lock)
|
||||
- `npm run lock:health -- --cadence daily` (relay websocket + publish/read probe; writes history to `task-logs/relay-health/`)
|
||||
- `npm run lock:complete` (manually complete a task)
|
||||
- `npm run dashboard:serve`
|
||||
- `npm test` (run unit tests)
|
||||
- `npm run test:unit:lock-backend` (run lock backend unit tests)
|
||||
- `npm run test:extended-main` (run extended integration tests)
|
||||
- `npm run validate:scheduler` (validate scheduler roster and prompts)
|
||||
- `npm run lint` (run linter)
|
||||
- `npm run lock:complete` (manually complete a task)
|
||||
- `npm run scheduler:daily` (run full daily scheduler cycle)
|
||||
- `npm run scheduler:weekly` (run full weekly scheduler cycle)
|
||||
- `npm run report:lock-reliability` (aggregate recent scheduler logs into markdown+JSON reliability reports)
|
||||
- `npm run torch:memory:list` (list memories)
|
||||
- `npm run torch:memory:inspect` (inspect a memory)
|
||||
- `npm run torch:remove` (remove TORCH from the project)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -382,10 +382,10 @@ Validation behavior:
|
||||
The scheduler is the primary entry point for automated agent execution. It handles lock acquisition, environment setup, agent handoff, and artifact verification.
|
||||
|
||||
```bash
|
||||
# Run the daily rotation
|
||||
# Run full daily scheduler cycle
|
||||
npm run scheduler:daily
|
||||
|
||||
# Run the weekly rotation
|
||||
# Run full weekly scheduler cycle
|
||||
npm run scheduler:weekly
|
||||
```
|
||||
|
||||
@@ -544,11 +544,18 @@ If you are developing `torch-lock` itself:
|
||||
- `npm run lock:check:daily`
|
||||
- `npm run lock:check:weekly`
|
||||
- `npm run lock:list`
|
||||
- `npm run lock:lock` (manually acquire a lock)
|
||||
- `npm run lock:health -- --cadence daily` (relay websocket + publish/read probe; writes history to `task-logs/relay-health/`)
|
||||
- `npm run lock:complete` (manually complete a task)
|
||||
- `npm run dashboard:serve`
|
||||
- `npm test` (run unit tests)
|
||||
- `npm run test:unit:lock-backend` (run lock backend unit tests)
|
||||
- `npm run test:extended-main` (run extended integration tests)
|
||||
- `npm run validate:scheduler` (validate scheduler roster and prompts)
|
||||
- `npm run lint` (run linter)
|
||||
- `npm run lock:complete` (manually complete a task)
|
||||
- `npm run scheduler:daily` (run full daily scheduler cycle)
|
||||
- `npm run scheduler:weekly` (run full weekly scheduler cycle)
|
||||
- `npm run report:lock-reliability` (aggregate recent scheduler logs into markdown+JSON reliability reports)
|
||||
- `npm run torch:memory:list` (list memories)
|
||||
- `npm run torch:memory:inspect` (inspect a memory)
|
||||
- `npm run torch:remove` (remove TORCH from the project)
|
||||
@@ -0,0 +1,11 @@
|
||||
# Memory Update — onboarding-audit-agent — 2026-02-20
|
||||
|
||||
## Key findings
|
||||
- Onboarding flow (`npm ci` -> build -> test -> lint) is stable and accurate.
|
||||
- `npm test` runs 181 tests successfully.
|
||||
|
||||
## Patterns / reusable knowledge
|
||||
- No special environment tweaks were needed for this run.
|
||||
|
||||
## Warnings / gotchas
|
||||
- Ensure `nostr-tools` is installed via `npm ci` before running lock checks.
|
||||
@@ -0,0 +1,12 @@
|
||||
# Memory Update — perf-agent — 2026-02-20
|
||||
|
||||
## Key findings
|
||||
- Initial scan found 22 performance-relevant patterns (async/timers).
|
||||
- Identified potential unbounded concurrency in `src/services/memory` (formatter, retriever, pruner).
|
||||
|
||||
## Patterns / reusable knowledge
|
||||
- `Promise.all(array.map(...))` is the most common pattern needing scrutiny.
|
||||
- `scripts/perf-search.mjs` is effective for baseline scanning.
|
||||
|
||||
## Warnings / gotchas
|
||||
- Memory service arrays might grow large, posing a risk without concurrency limits.
|
||||
@@ -0,0 +1,12 @@
|
||||
# Memory Update — protocol-research-agent — 2026-02-20
|
||||
|
||||
## Key findings
|
||||
- `nostr-tools` version in `PROTOCOL_INVENTORY.md` was outdated (`2.19.4` vs `2.23.1` in `package.json`).
|
||||
- `src/relay-health.mjs` implements a custom protocol (Kind 27235) for deep relay health checks.
|
||||
|
||||
## Patterns / reusable knowledge
|
||||
- Always check `package.json` for current library versions when updating inventories.
|
||||
- `grep` for "Kind" and "NIP" is effective for finding protocol implementations.
|
||||
|
||||
## Warnings / gotchas
|
||||
- `grep` results often include test logs; use exclude flags to reduce noise.
|
||||
@@ -1,4 +1,5 @@
|
||||
# Initial Baseline
|
||||
# Initial Performance Baseline
|
||||
|
||||
- Date: 2026-02-15
|
||||
- Initial metrics established from static analysis.
|
||||
## Date: 2026-02-20
|
||||
|
||||
Establishing baseline via search patterns.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Daily Performance Report - 2026-02-20
|
||||
|
||||
## Summary
|
||||
Initial baseline scan completed. Identified 22 instances of async/background patterns. No critical (P0) login-blocking issues found, but several potential unbounded concurrency issues identified in the memory service.
|
||||
|
||||
## Findings
|
||||
|
||||
### P1: Potential Unbounded Concurrency in Memory Service
|
||||
Several locations use `Promise.all(array.map(...))` without concurrency limits. If the arrays (memories, actions) grow large, this could spike CPU/Network usage.
|
||||
|
||||
- **File**: `src/services/memory/formatter.js:181`
|
||||
- `await Promise.all((memories ?? []).map((memory) => memoryService.updateMemoryUsage...))`
|
||||
- **File**: `src/services/memory/retriever.js:256`
|
||||
- `await Promise.all(ids.map((id) => repository.updateMemoryUsage...))`
|
||||
- **File**: `src/services/memory/pruner.js:276`
|
||||
- `await Promise.all(plan.actions.map(async (decision) => ...))`
|
||||
|
||||
**Recommendation**: Replace with a concurrency-limited runner (e.g., `p-limit` or a utility function) if these arrays are expected to exceed ~50 items.
|
||||
|
||||
### Other Hits
|
||||
- Standard `setTimeout`/`Promise.race` usage in lock/relay logic (expected).
|
||||
- `setInterval` in `src/services/memory/scheduler.js` (expected).
|
||||
|
||||
## Metrics
|
||||
- **Total Hits**: 22
|
||||
- **P0 Issues**: 0
|
||||
- **P1 Issues**: 3 (Potential)
|
||||
|
||||
## Actions Taken
|
||||
- Established baseline `hits-2026-02-20.json`.
|
||||
- Documented findings.
|
||||
|
||||
## Next Steps
|
||||
- Investigate array sizes in memory service to determine if concurrency limits are strictly necessary.
|
||||
- Continue monitoring for new patterns.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Protocol Report - 2026-02-20
|
||||
|
||||
## Summary
|
||||
The protocol research agent scanned the codebase for external protocol dependencies and compliance. No new protocols were identified. Existing implementations remain compliant with their respective specifications.
|
||||
|
||||
## Findings
|
||||
|
||||
### 1. Nostr Protocol Compliance
|
||||
- **NIP-01**: Implemented via `nostr-tools` and `src/lock-ops.mjs`.
|
||||
- **NIP-33**: Implemented via `d` tag usage in lock events.
|
||||
- **NIP-40**: Implemented via `expiration` tag in lock events.
|
||||
- **NIP-78**: Implemented via Kind 30078 for lock storage.
|
||||
- **Custom**: Kind 27235 used for relay health probes.
|
||||
|
||||
### 2. Dependency Updates
|
||||
- `nostr-tools` version updated from `2.19.4` to `2.23.1` in `PROTOCOL_INVENTORY.md` to match `package.json`.
|
||||
|
||||
### 3. Risk Assessment
|
||||
- **Auth/Crypto**: Uses `nostr-tools` (audited external lib) for signing. No custom crypto implementation found.
|
||||
- **Network**: Uses standard `ws` library for WebSocket connections.
|
||||
|
||||
## Recommendations
|
||||
- Continue to pin `nostr-tools` version.
|
||||
- Periodically review `nostr-tools` changelogs for security updates.
|
||||
@@ -0,0 +1,51 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const patterns = [
|
||||
/setInterval|setTimeout|requestAnimationFrame|requestIdleCallback/i,
|
||||
/Promise\.allSettled|Promise\.all|Promise\.any|Promise\.race/i,
|
||||
/new Worker|Worker\(|postMessage\(|getDmDecryptWorkerQueueSize|decryptDmInWorker/i,
|
||||
/new WebTorrent|WebTorrent|torrent|magnet|torrentHash|magnetValidators/i,
|
||||
/integrationClient\.pool|publishEventToRelays|pool\.list|queueSignEvent|relayManager|authService|hydrateFromStorage/i,
|
||||
/document\.hidden|visibilitychange/i
|
||||
];
|
||||
|
||||
async function scanDir(dir) {
|
||||
const hits = [];
|
||||
try {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
|
||||
hits.push(...await scanDir(fullPath));
|
||||
} else if (entry.isFile() && /\.(mjs|js|jsx|ts|tsx)$/.test(entry.name)) {
|
||||
const content = await fs.readFile(fullPath, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
for (const pattern of patterns) {
|
||||
if (pattern.test(line)) {
|
||||
hits.push({
|
||||
file: fullPath,
|
||||
line: index + 1,
|
||||
content: line.trim(),
|
||||
pattern: pattern.toString()
|
||||
});
|
||||
break; // One hit per line is enough
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') console.error(`Error scanning ${dir}:`, err);
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
|
||||
const hits = await scanDir('src');
|
||||
const outputFile = process.argv[2] || 'reports/performance/hits.json';
|
||||
await fs.writeFile(outputFile, JSON.stringify(hits, null, 2));
|
||||
console.log(`Wrote ${hits.length} hits to ${outputFile}`);
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
agent: onboarding-audit-agent
|
||||
cadence: daily
|
||||
run-start: 2026-02-20T18:30:00Z
|
||||
prompt: src/prompts/daily/onboarding-audit-agent.md
|
||||
---
|
||||
# Context - Onboarding Audit
|
||||
|
||||
## Goal
|
||||
Verify that a fresh developer can onboard successfully using the documentation.
|
||||
|
||||
## Scope
|
||||
- `README.md`
|
||||
- `CONTRIBUTING.md`
|
||||
- Onboarding scripts (`npm ci`, `npm test`, `npm run build`)
|
||||
|
||||
## Constraints
|
||||
- Must use a clean environment simulation.
|
||||
- Must not modify `package.json` unless necessary.
|
||||
|
||||
## References
|
||||
- Agent: `onboarding-audit-agent`
|
||||
- Prompt: `src/prompts/daily/onboarding-audit-agent.md`
|
||||
@@ -0,0 +1,21 @@
|
||||
# Context - perf-agent - 2026-02-20
|
||||
|
||||
Agent: perf-agent
|
||||
Cadence: daily
|
||||
Run-Start: 2026-02-20T19:31:14Z
|
||||
Prompt: src/prompts/daily/perf-agent.md
|
||||
|
||||
## Goal
|
||||
Establish a performance baseline and identify P0/P1 issues using search patterns.
|
||||
|
||||
## Scope
|
||||
- Scan codebase for performance-critical patterns (intervals, workers, etc.).
|
||||
- Generate hits report.
|
||||
- Audit /content documentation if needed (deferred to analysis).
|
||||
|
||||
## Constraints
|
||||
- Minimal changes for baseline establishment.
|
||||
- Must run search script.
|
||||
|
||||
## Assumptions
|
||||
- This is the first run (or treated as such for baseline).
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
Agent: protocol-research-agent
|
||||
Cadence: daily
|
||||
Run-Start: 2026-02-20T21-17-56Z
|
||||
Prompt: src/prompts/daily/protocol-research-agent.md
|
||||
---
|
||||
|
||||
# Context
|
||||
|
||||
## Goal
|
||||
Identify external protocol/spec dependencies used by this codebase, map them to implementation points, assess compliance risk, and propose the smallest safe improvements.
|
||||
|
||||
## Scope
|
||||
- `PROTOCOL_INVENTORY.md`
|
||||
- Codebase scan for protocol references
|
||||
|
||||
## Assumptions
|
||||
- `grep` is sufficient for initial discovery of protocol references.
|
||||
|
||||
## Constraints
|
||||
- Prefer incremental changes.
|
||||
- Treat auth, cryptography, and trust boundaries as high risk.
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
agent: onboarding-audit-agent
|
||||
cadence: daily
|
||||
run-start: 2026-02-20T18:30:00Z
|
||||
prompt: src/prompts/daily/onboarding-audit-agent.md
|
||||
---
|
||||
# Decisions - Onboarding Audit
|
||||
|
||||
- **Decision**: No changes to `README.md` or `CONTRIBUTING.md`.
|
||||
- **Rationale**: Current instructions are accurate and functional. All commands passed as documented.
|
||||
|
||||
## References
|
||||
- Agent: `onboarding-audit-agent`
|
||||
- Prompt: `src/prompts/daily/onboarding-audit-agent.md`
|
||||
@@ -0,0 +1,10 @@
|
||||
# Decisions - perf-agent - 2026-02-20
|
||||
|
||||
Agent: perf-agent
|
||||
Cadence: daily
|
||||
Run-Start: 2026-02-20T19:31:14Z
|
||||
Prompt: src/prompts/daily/perf-agent.md
|
||||
|
||||
## Key Decision
|
||||
- **Baseline Scan**: Decided to run a full scan to establish a baseline `hits.json` file.
|
||||
- **Rationale**: Need to know the current state before prioritizing fixes.
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
Agent: protocol-research-agent
|
||||
Cadence: daily
|
||||
Run-Start: 2026-02-20T21-17-56Z
|
||||
Prompt: src/prompts/daily/protocol-research-agent.md
|
||||
---
|
||||
|
||||
# Decisions
|
||||
|
||||
## Decision 1: Update nostr-tools version
|
||||
- **Context**: `PROTOCOL_INVENTORY.md` listed `2.19.4` but `package.json` had `2.23.1`.
|
||||
- **Decision**: Update `PROTOCOL_INVENTORY.md` to match `package.json`.
|
||||
- **Rationale**: Inventory must reflect the actual codebase state.
|
||||
+168
-199
@@ -524,6 +524,167 @@ export async function cmdComplete(agent, cadence, optionsOrDryRun = false, deps
|
||||
return { status: 'completed', eventId: event.id };
|
||||
}
|
||||
|
||||
const COMMAND_HANDLERS = {
|
||||
check: async (args) => {
|
||||
if (!args.cadence || !VALID_CADENCES.has(args.cadence)) {
|
||||
console.error(`ERROR: --cadence <${[...VALID_CADENCES].join('|')}> is required for check`);
|
||||
throw new ExitError(1, 'Missing cadence');
|
||||
}
|
||||
await cmdCheck(args.cadence, {
|
||||
logDir: args.logDir,
|
||||
ignoreLogs: args.ignoreLogs,
|
||||
json: args.json,
|
||||
jsonFile: args.jsonFile,
|
||||
quiet: args.quiet,
|
||||
});
|
||||
},
|
||||
lock: async (args) => {
|
||||
if (!args.agent) {
|
||||
console.error('ERROR: --agent <name> is required for lock');
|
||||
throw new ExitError(1, 'Missing agent');
|
||||
}
|
||||
if (!args.cadence || !VALID_CADENCES.has(args.cadence)) {
|
||||
console.error(`ERROR: --cadence <${[...VALID_CADENCES].join('|')}> is required for lock`);
|
||||
throw new ExitError(1, 'Missing cadence');
|
||||
}
|
||||
await cmdLock(args.agent, args.cadence, {
|
||||
dryRun: args.dryRun,
|
||||
platform: args.platform,
|
||||
model: args.model
|
||||
});
|
||||
},
|
||||
complete: async (args) => {
|
||||
if (!args.agent) {
|
||||
console.error('ERROR: --agent <name> is required for complete');
|
||||
throw new ExitError(1, 'Missing agent');
|
||||
}
|
||||
if (!args.cadence || !VALID_CADENCES.has(args.cadence)) {
|
||||
console.error(`ERROR: --cadence <${[...VALID_CADENCES].join('|')}> is required for complete`);
|
||||
throw new ExitError(1, 'Missing cadence');
|
||||
}
|
||||
await cmdComplete(args.agent, args.cadence, {
|
||||
dryRun: args.dryRun,
|
||||
platform: args.platform,
|
||||
model: args.model
|
||||
});
|
||||
},
|
||||
list: async (args) => {
|
||||
if (args.cadence && !VALID_CADENCES.has(args.cadence)) {
|
||||
console.error(`ERROR: --cadence must be one of: ${[...VALID_CADENCES].join(', ')}`);
|
||||
throw new ExitError(1, 'Invalid cadence');
|
||||
}
|
||||
await cmdList(args.cadence || null);
|
||||
},
|
||||
health: async (args) => {
|
||||
if (!args.cadence || !VALID_CADENCES.has(args.cadence)) {
|
||||
console.error(`ERROR: --cadence <${[...VALID_CADENCES].join('|')}> is required for health`);
|
||||
throw new ExitError(1, 'Missing cadence');
|
||||
}
|
||||
const result = await runRelayHealthCheck({
|
||||
cadence: args.cadence,
|
||||
...(Number.isFinite(args.timeoutMs) ? { timeoutMs: args.timeoutMs } : {}),
|
||||
...(Number.isFinite(args.allRelaysDownMinutes) ? { allRelaysDownMinutes: args.allRelaysDownMinutes } : {}),
|
||||
...(Number.isFinite(args.minSuccessRate) ? { minSuccessRate: args.minSuccessRate } : {}),
|
||||
...(Number.isFinite(args.windowMinutes) ? { windowMinutes: args.windowMinutes } : {}),
|
||||
});
|
||||
if (!result.ok) {
|
||||
result.failureCategory = 'all relays unhealthy';
|
||||
}
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
if (!result.ok) throw new ExitError(2, 'Relay health check failed');
|
||||
},
|
||||
dashboard: async (args) => {
|
||||
await cmdDashboard(args.port, args.host);
|
||||
},
|
||||
init: async (args) => {
|
||||
await cmdInit(args.force);
|
||||
},
|
||||
update: async (args) => {
|
||||
await cmdUpdate(args.force);
|
||||
},
|
||||
remove: async (args) => {
|
||||
await cmdRemove(args.force);
|
||||
},
|
||||
'list-memories': async (args) => {
|
||||
const result = await listMemories({
|
||||
agent_id: args.agent,
|
||||
type: args.type,
|
||||
tags: args.tags,
|
||||
pinned: args.pinned,
|
||||
limit: args.limit,
|
||||
offset: args.offset,
|
||||
});
|
||||
|
||||
if (!args.full && Array.isArray(result)) {
|
||||
for (const memory of result) {
|
||||
if (typeof memory.content === 'string' && memory.content.length > 200) {
|
||||
memory.content = memory.content.slice(0, 200) + '... (truncated, use --full to see all)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
},
|
||||
'inspect-memory': async (args) => {
|
||||
if (!args.id) {
|
||||
console.error('ERROR: --id <memoryId> is required for inspect-memory');
|
||||
throw new ExitError(1, 'Missing memory id');
|
||||
}
|
||||
const result = await inspectMemory(args.id);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
},
|
||||
'pin-memory': async (args) => {
|
||||
if (!args.id) {
|
||||
console.error('ERROR: --id <memoryId> is required for pin-memory');
|
||||
throw new ExitError(1, 'Missing memory id');
|
||||
}
|
||||
const result = await pinMemory(args.id);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
},
|
||||
'unpin-memory': async (args) => {
|
||||
if (!args.id) {
|
||||
console.error('ERROR: --id <memoryId> is required for unpin-memory');
|
||||
throw new ExitError(1, 'Missing memory id');
|
||||
}
|
||||
const result = await unpinMemory(args.id);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
},
|
||||
'trigger-prune-dry-run': async (args) => {
|
||||
const result = await triggerPruneDryRun({ retentionMs: args.retentionMs ?? undefined });
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
},
|
||||
'memory-stats': async (args) => {
|
||||
const result = await memoryStats({ windowMs: args.windowMs ?? undefined });
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
},
|
||||
proposal: async (args) => {
|
||||
if (!args.subcommand) {
|
||||
console.error('ERROR: Missing subcommand for proposal (create, list, apply, reject, show)');
|
||||
throw new ExitError(1, 'Missing subcommand');
|
||||
}
|
||||
await cmdProposal(args.subcommand, {
|
||||
agent: args.agent,
|
||||
target: args.target,
|
||||
contentFile: args.content,
|
||||
reason: args.reason,
|
||||
id: args.id,
|
||||
status: args.status
|
||||
});
|
||||
},
|
||||
rollback: async (args) => {
|
||||
await cmdRollback(args.target, args.strategy, { list: args.list });
|
||||
},
|
||||
backup: async (args) => {
|
||||
const { cmdBackup, listBackups } = await import('./cmd-backup.mjs');
|
||||
if (args.list) {
|
||||
const backups = await listBackups();
|
||||
console.log(JSON.stringify(backups, null, 2));
|
||||
} else {
|
||||
await cmdBackup({ output: args.output });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function usage() {
|
||||
console.error(USAGE_TEXT);
|
||||
}
|
||||
@@ -543,205 +704,13 @@ export async function main(argv) {
|
||||
throw new ExitError(1, 'No command specified');
|
||||
}
|
||||
|
||||
switch (args.command) {
|
||||
case 'check': {
|
||||
if (!args.cadence || !VALID_CADENCES.has(args.cadence)) {
|
||||
console.error(`ERROR: --cadence <${[...VALID_CADENCES].join('|')}> is required for check`);
|
||||
throw new ExitError(1, 'Missing cadence');
|
||||
}
|
||||
await cmdCheck(args.cadence, {
|
||||
logDir: args.logDir,
|
||||
ignoreLogs: args.ignoreLogs,
|
||||
json: args.json,
|
||||
jsonFile: args.jsonFile,
|
||||
quiet: args.quiet,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'lock': {
|
||||
if (!args.agent) {
|
||||
console.error('ERROR: --agent <name> is required for lock');
|
||||
throw new ExitError(1, 'Missing agent');
|
||||
}
|
||||
if (!args.cadence || !VALID_CADENCES.has(args.cadence)) {
|
||||
console.error(`ERROR: --cadence <${[...VALID_CADENCES].join('|')}> is required for lock`);
|
||||
throw new ExitError(1, 'Missing cadence');
|
||||
}
|
||||
await cmdLock(args.agent, args.cadence, {
|
||||
dryRun: args.dryRun,
|
||||
platform: args.platform,
|
||||
model: args.model
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'complete': {
|
||||
if (!args.agent) {
|
||||
console.error('ERROR: --agent <name> is required for complete');
|
||||
throw new ExitError(1, 'Missing agent');
|
||||
}
|
||||
if (!args.cadence || !VALID_CADENCES.has(args.cadence)) {
|
||||
console.error(`ERROR: --cadence <${[...VALID_CADENCES].join('|')}> is required for complete`);
|
||||
throw new ExitError(1, 'Missing cadence');
|
||||
}
|
||||
await cmdComplete(args.agent, args.cadence, {
|
||||
dryRun: args.dryRun,
|
||||
platform: args.platform,
|
||||
model: args.model
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'list': {
|
||||
if (args.cadence && !VALID_CADENCES.has(args.cadence)) {
|
||||
console.error(`ERROR: --cadence must be one of: ${[...VALID_CADENCES].join(', ')}`);
|
||||
throw new ExitError(1, 'Invalid cadence');
|
||||
}
|
||||
await cmdList(args.cadence || null);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'health': {
|
||||
if (!args.cadence || !VALID_CADENCES.has(args.cadence)) {
|
||||
console.error(`ERROR: --cadence <${[...VALID_CADENCES].join('|')}> is required for health`);
|
||||
throw new ExitError(1, 'Missing cadence');
|
||||
}
|
||||
const result = await runRelayHealthCheck({
|
||||
cadence: args.cadence,
|
||||
...(Number.isFinite(args.timeoutMs) ? { timeoutMs: args.timeoutMs } : {}),
|
||||
...(Number.isFinite(args.allRelaysDownMinutes) ? { allRelaysDownMinutes: args.allRelaysDownMinutes } : {}),
|
||||
...(Number.isFinite(args.minSuccessRate) ? { minSuccessRate: args.minSuccessRate } : {}),
|
||||
...(Number.isFinite(args.windowMinutes) ? { windowMinutes: args.windowMinutes } : {}),
|
||||
});
|
||||
if (!result.ok) {
|
||||
result.failureCategory = 'all relays unhealthy';
|
||||
}
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
if (!result.ok) throw new ExitError(2, 'Relay health check failed');
|
||||
break;
|
||||
}
|
||||
|
||||
case 'dashboard': {
|
||||
await cmdDashboard(args.port, args.host);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'init': {
|
||||
await cmdInit(args.force);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'update': {
|
||||
await cmdUpdate(args.force);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'remove': {
|
||||
await cmdRemove(args.force);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'list-memories': {
|
||||
const result = await listMemories({
|
||||
agent_id: args.agent,
|
||||
type: args.type,
|
||||
tags: args.tags,
|
||||
pinned: args.pinned,
|
||||
limit: args.limit,
|
||||
offset: args.offset,
|
||||
});
|
||||
|
||||
if (!args.full && Array.isArray(result)) {
|
||||
for (const memory of result) {
|
||||
if (typeof memory.content === 'string' && memory.content.length > 200) {
|
||||
memory.content = memory.content.slice(0, 200) + '... (truncated, use --full to see all)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'inspect-memory': {
|
||||
if (!args.id) {
|
||||
console.error('ERROR: --id <memoryId> is required for inspect-memory');
|
||||
throw new ExitError(1, 'Missing memory id');
|
||||
}
|
||||
const result = await inspectMemory(args.id);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'pin-memory': {
|
||||
if (!args.id) {
|
||||
console.error('ERROR: --id <memoryId> is required for pin-memory');
|
||||
throw new ExitError(1, 'Missing memory id');
|
||||
}
|
||||
const result = await pinMemory(args.id);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'unpin-memory': {
|
||||
if (!args.id) {
|
||||
console.error('ERROR: --id <memoryId> is required for unpin-memory');
|
||||
throw new ExitError(1, 'Missing memory id');
|
||||
}
|
||||
const result = await unpinMemory(args.id);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'trigger-prune-dry-run': {
|
||||
const result = await triggerPruneDryRun({ retentionMs: args.retentionMs ?? undefined });
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'memory-stats': {
|
||||
const result = await memoryStats({ windowMs: args.windowMs ?? undefined });
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'proposal': {
|
||||
if (!args.subcommand) {
|
||||
console.error('ERROR: Missing subcommand for proposal (create, list, apply, reject, show)');
|
||||
throw new ExitError(1, 'Missing subcommand');
|
||||
}
|
||||
await cmdProposal(args.subcommand, {
|
||||
agent: args.agent,
|
||||
target: args.target,
|
||||
contentFile: args.content,
|
||||
reason: args.reason,
|
||||
id: args.id,
|
||||
status: args.status
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'rollback': {
|
||||
await cmdRollback(args.target, args.strategy, { list: args.list });
|
||||
break;
|
||||
}
|
||||
|
||||
case 'backup': {
|
||||
const { cmdBackup, listBackups } = await import('./cmd-backup.mjs');
|
||||
if (args.list) {
|
||||
const backups = await listBackups();
|
||||
console.log(JSON.stringify(backups, null, 2));
|
||||
} else {
|
||||
await cmdBackup({ output: args.output });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.error(`ERROR: Unknown command: ${args.command}`);
|
||||
usage();
|
||||
throw new ExitError(1, 'Unknown command');
|
||||
const handler = COMMAND_HANDLERS[args.command];
|
||||
if (handler && Object.hasOwn(COMMAND_HANDLERS, args.command)) {
|
||||
await handler(args);
|
||||
} else {
|
||||
console.error(`ERROR: Unknown command: ${args.command}`);
|
||||
usage();
|
||||
throw new ExitError(1, 'Unknown command');
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof ExitError) {
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import {
|
||||
KIND_APP_DATA,
|
||||
MS_PER_SECOND,
|
||||
} from './constants.mjs';
|
||||
import { nowUnix } from './utils.mjs';
|
||||
import { nowUnix, mergeRelayList } from './utils.mjs';
|
||||
import { defaultHealthManager, buildRelayHealthConfig, RelayHealthManager } from './relay-health-manager.mjs';
|
||||
import { publishLock, LockPublisher } from './lock-publisher.mjs';
|
||||
import {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
getRelayFallbacks,
|
||||
getMinActiveRelayPool,
|
||||
} from './torch-config.mjs';
|
||||
import { mergeRelayList } from './utils.mjs';
|
||||
import { defaultHealthManager, buildRelayHealthConfig } from './relay-health-manager.mjs';
|
||||
import {
|
||||
withTimeout,
|
||||
|
||||
+1
-9
@@ -49,15 +49,7 @@ export async function getCompletedAgents(cadence, logDir, deps) {
|
||||
return completed;
|
||||
}
|
||||
|
||||
export function withTimeout(promise, timeoutMs, timeoutMessage) {
|
||||
let timeoutHandle;
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
timeoutHandle = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
|
||||
});
|
||||
return Promise.race([promise, timeoutPromise]).finally(() => {
|
||||
if (timeoutHandle) clearTimeout(timeoutHandle);
|
||||
});
|
||||
}
|
||||
export { withTimeout };
|
||||
|
||||
export function relayListLabel(relays) {
|
||||
return relays.join(', ');
|
||||
|
||||
@@ -4,17 +4,10 @@ import WebSocket from 'ws';
|
||||
import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools/pure';
|
||||
import { getNamespace, getRelays } from './torch-config.mjs';
|
||||
import { MS_PER_SECOND } from './constants.mjs';
|
||||
import { withTimeout } from './utils.mjs';
|
||||
|
||||
const PROBE_KIND = 27235;
|
||||
|
||||
function withTimeout(promise, timeoutMs, message) {
|
||||
let timeoutId;
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs);
|
||||
});
|
||||
return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
|
||||
}
|
||||
|
||||
function nowIso() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
@@ -132,8 +132,14 @@ export async function createProposal({ agent, target, newContent, reason }) {
|
||||
return { id, diff };
|
||||
}
|
||||
|
||||
export async function validateProposal(id) {
|
||||
const { meta, newContent } = await getProposal(id);
|
||||
export async function validateProposal(proposalOrId) {
|
||||
let proposal;
|
||||
if (typeof proposalOrId === 'string') {
|
||||
proposal = await getProposal(proposalOrId);
|
||||
} else {
|
||||
proposal = proposalOrId;
|
||||
}
|
||||
const { meta, newContent } = proposal;
|
||||
|
||||
// 1. Allowlist check (redundant but safe)
|
||||
const { isAllowed } = resolveAndValidateTarget(meta.target);
|
||||
@@ -165,7 +171,7 @@ export async function applyProposal(id) {
|
||||
throw new Error(`Proposal is ${meta.status}, cannot apply.`);
|
||||
}
|
||||
|
||||
const validation = await validateProposal(id);
|
||||
const validation = await validateProposal(proposal);
|
||||
if (!validation.valid) {
|
||||
// Mark as rejected if validation fails?
|
||||
// Or just throw? The governance agent should decide.
|
||||
|
||||
@@ -41,13 +41,24 @@ function loadMemoryStore() {
|
||||
let currentSavePromise = null;
|
||||
let pendingSavePromise = null;
|
||||
let pendingSaveResolve = null;
|
||||
let lastSaveTime = 0;
|
||||
const MIN_SAVE_INTERVAL_MS = 1000;
|
||||
|
||||
async function performSave(store) {
|
||||
try {
|
||||
const now = Date.now();
|
||||
const timeSinceLast = now - lastSaveTime;
|
||||
if (timeSinceLast < MIN_SAVE_INTERVAL_MS) {
|
||||
await new Promise((resolve) => setTimeout(resolve, MIN_SAVE_INTERVAL_MS - timeSinceLast));
|
||||
}
|
||||
|
||||
const dir = path.dirname(MEMORY_FILE_PATH);
|
||||
await fs.promises.mkdir(dir, { recursive: true });
|
||||
const entries = [...store.entries()];
|
||||
await fs.promises.writeFile(MEMORY_FILE_PATH, JSON.stringify(entries, null, 2), 'utf8');
|
||||
const tmpPath = `${MEMORY_FILE_PATH}.${Date.now()}.${Math.random()}.tmp`;
|
||||
await fs.promises.writeFile(tmpPath, JSON.stringify(entries, null, 2), 'utf8');
|
||||
await fs.promises.rename(tmpPath, MEMORY_FILE_PATH);
|
||||
lastSaveTime = Date.now();
|
||||
} catch (err) {
|
||||
console.error('Failed to save memory store:', err);
|
||||
} finally {
|
||||
@@ -105,7 +116,7 @@ const memoryRepository = {
|
||||
const updated = { ...existing, last_seen: lastSeen };
|
||||
memoryStore.set(id, updated);
|
||||
storeVersion++;
|
||||
await saveMemoryStore(memoryStore);
|
||||
saveMemoryStore(memoryStore).catch((err) => console.error('Failed to save memory usage update:', err));
|
||||
return updated;
|
||||
},
|
||||
async listPruneCandidates({ cutoff }) {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
agent: onboarding-audit-agent
|
||||
cadence: daily
|
||||
run-start: 2026-02-20T18:30:00Z
|
||||
prompt: src/prompts/daily/onboarding-audit-agent.md
|
||||
---
|
||||
# Test Log - Onboarding Audit
|
||||
|
||||
## Execution
|
||||
- **Command**: `npm ci`
|
||||
- **Result**: Success
|
||||
|
||||
- **Command**: `npm run build`
|
||||
- **Result**: Success
|
||||
|
||||
- **Command**: `npm test`
|
||||
- **Result**: Passed (181 tests)
|
||||
|
||||
- **Command**: `npm run lint`
|
||||
- **Result**: Passed
|
||||
|
||||
## References
|
||||
- Agent: `onboarding-audit-agent`
|
||||
- Prompt: `src/prompts/daily/onboarding-audit-agent.md`
|
||||
@@ -0,0 +1,13 @@
|
||||
# Test Log - perf-agent - 2026-02-20
|
||||
|
||||
Agent: perf-agent
|
||||
Cadence: daily
|
||||
Run-Start: 2026-02-20T19:31:14Z
|
||||
Prompt: src/prompts/daily/perf-agent.md
|
||||
|
||||
## Environment
|
||||
- Agent: perf-agent
|
||||
- Cadence: daily
|
||||
|
||||
- Command: node scripts/perf-search.mjs
|
||||
- Result: Success (22 hits found).
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
Agent: protocol-research-agent
|
||||
Cadence: daily
|
||||
Run-Start: 2026-02-20T21-17-56Z
|
||||
Prompt: src/prompts/daily/protocol-research-agent.md
|
||||
---
|
||||
|
||||
# Test Log
|
||||
|
||||
## Command: grep for protocols
|
||||
- **Command**: `grep -rE "protocol|spec|standard|rfc|cip|bip|nip" src/`
|
||||
- **Result**: Found existing usages in `src/lock-ops.mjs` and `src/relay-health.mjs`. No new protocols found.
|
||||
|
||||
## Command: verify nostr-tools version
|
||||
- **Command**: Checked `package.json` vs `PROTOCOL_INVENTORY.md`
|
||||
- **Result**: Version mismatch identified and fixed.
|
||||
@@ -20,7 +20,7 @@ prompt: src/prompts/daily/audit-agent.md
|
||||
- [ ] Create audit-report-2026-02-19.md
|
||||
- [ ] Post summary to GitHub (issue/PR comment)
|
||||
- [ ] Run memory store (MEMORY_STORED)
|
||||
- [ ] Run npm run lint (repo validation)
|
||||
- [x] Run npm run lint (repo validation)
|
||||
|
||||
## Completed tasks
|
||||
- Read TORCH.md ✓
|
||||
|
||||
@@ -7,11 +7,11 @@ Run Start: 2026-02-19T22-29-56Z
|
||||
Run ID: session-1771540169
|
||||
|
||||
## Pending
|
||||
- [ ] Verify claims against code (N/A - no claims found).
|
||||
- [ ] Update documentation (N/A - no claims found).
|
||||
- [ ] Validate changes (N/A - no changes made).
|
||||
|
||||
## Completed
|
||||
- [x] Verify claims against code (N/A - no claims found).
|
||||
- [x] Update documentation (N/A - no claims found).
|
||||
- [x] Validate changes (N/A - no changes made).
|
||||
- [x] Inventory `docs/` for relevant claims.
|
||||
- [x] Check for user-facing upload/contribution guides.
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# TODO: Decompose src/lock-ops.mjs
|
||||
|
||||
- [ ] Extract `RelayHealthManager` and helpers to `src/relay-health-manager.mjs`
|
||||
- [ ] Extract `LockPublisher` and helpers to `src/lock-publisher.mjs`
|
||||
- [ ] Refactor `src/lock-ops.mjs` to import new modules
|
||||
- [ ] Verify linting passes
|
||||
- [ ] Verify tests pass (`npm test`)
|
||||
- [ ] Verify file size reduction
|
||||
- [x] Extract `RelayHealthManager` and helpers to `src/relay-health-manager.mjs`
|
||||
- [x] Extract `LockPublisher` and helpers to `src/lock-publisher.mjs`
|
||||
- [x] Refactor `src/lock-ops.mjs` to import new modules
|
||||
- [x] Verify linting passes
|
||||
- [x] Verify tests pass (`npm test`)
|
||||
- [x] Verify file size reduction
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
agent: onboarding-audit-agent
|
||||
cadence: daily
|
||||
run-start: 2026-02-20T18:30:00Z
|
||||
prompt: src/prompts/daily/onboarding-audit-agent.md
|
||||
---
|
||||
# Todo - Onboarding Audit
|
||||
|
||||
## Completed Tasks
|
||||
- [x] Audit onboarding steps
|
||||
- [x] Verify docs
|
||||
|
||||
## Pending Tasks
|
||||
- None
|
||||
|
||||
## References
|
||||
- Agent: `onboarding-audit-agent`
|
||||
- Prompt: `src/prompts/daily/onboarding-audit-agent.md`
|
||||
@@ -0,0 +1,16 @@
|
||||
# TODO - perf-agent - 2026-02-20
|
||||
|
||||
Agent: perf-agent
|
||||
Cadence: daily
|
||||
Run-Start: 2026-02-20T19:31:14Z
|
||||
Prompt: src/prompts/daily/perf-agent.md
|
||||
|
||||
## Done
|
||||
- [x] Create run artifacts.
|
||||
- [x] Run memory retrieval.
|
||||
- [x] Run search patterns.
|
||||
- [x] Generate daily report.
|
||||
- [x] Store memory.
|
||||
|
||||
## Pending
|
||||
- [ ] Analyze hits in depth (next run).
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
Agent: protocol-research-agent
|
||||
Cadence: daily
|
||||
Run-Start: 2026-02-20T21-17-56Z
|
||||
Prompt: src/prompts/daily/protocol-research-agent.md
|
||||
---
|
||||
|
||||
# Todo
|
||||
|
||||
## Pending
|
||||
- [ ] Scan repo for protocol/spec references
|
||||
- [ ] Update PROTOCOL_INVENTORY.md
|
||||
- [ ] Create protocol report
|
||||
@@ -42,8 +42,22 @@ export function detectPlatform() {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function mergeRelayList(primaryRelays, fallbackRelays) {
|
||||
return [...new Set([...primaryRelays, ...fallbackRelays])];
|
||||
}
|
||||
|
||||
export function ensureDir(dir) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
export function withTimeout(promise, timeoutMs, timeoutMessage) {
|
||||
let timeoutHandle;
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
timeoutHandle = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
|
||||
});
|
||||
return Promise.race([promise, timeoutPromise]).finally(() => {
|
||||
if (timeoutHandle) clearTimeout(timeoutHandle);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
agent: onboarding-audit-agent
|
||||
cadence: daily
|
||||
run-start: 2026-02-20T18:30:00Z
|
||||
platform: jules
|
||||
status: completed
|
||||
---
|
||||
# Task Log: onboarding-audit-agent (completed)
|
||||
|
||||
## Summary
|
||||
Onboarding audit successful. All steps passed verification.
|
||||
|
||||
## Details
|
||||
- **Lint**: Passed
|
||||
- **Tests**: Passed (181 tests)
|
||||
- **Artifacts**: Verified
|
||||
- **Memory**: Retrieved and Stored
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
agent: perf-agent
|
||||
cadence: daily
|
||||
date: 2026-02-20
|
||||
platform: jules
|
||||
status: completed
|
||||
run-start: 2026-02-20T19:31:14Z
|
||||
---
|
||||
|
||||
# Task Log - perf-agent - 2026-02-20
|
||||
|
||||
## Summary
|
||||
- Established performance baseline.
|
||||
- Scanned codebase for async patterns.
|
||||
- Generated `reports/performance/hits-2026-02-20.json` (22 hits).
|
||||
- Identified potential unbounded concurrency in `src/services/memory`.
|
||||
|
||||
## Artifacts
|
||||
- `reports/performance/hits-2026-02-20.json`
|
||||
- `reports/performance/daily-perf-report-2026-02-20.md`
|
||||
- `src/context/CONTEXT_2026-02-20T19-31-14Z.md`
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
agent: protocol-research-agent
|
||||
cadence: daily
|
||||
platform: jules
|
||||
---
|
||||
|
||||
# Run Summary
|
||||
|
||||
**Protocol Research Agent** completed its daily cycle.
|
||||
|
||||
## Achievements
|
||||
- Verified `PROTOCOL_INVENTORY.md` accuracy.
|
||||
- Updated `nostr-tools` version in inventory.
|
||||
- Created protocol report `reports/protocol/protocol-report-2026-02-20.md`.
|
||||
- No new protocols identified.
|
||||
- Existing protocols remain compliant.
|
||||
|
||||
## Artifacts
|
||||
- Report: `reports/protocol/protocol-report-2026-02-20.md`
|
||||
- Memory Update: `memory-updates/2026-02-20T21-17-56Z__protocol-research-agent.md`
|
||||
@@ -0,0 +1,98 @@
|
||||
import { mock, describe, it, before, after, beforeEach } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
|
||||
// Mock process.exit
|
||||
const originalExit = process.exit;
|
||||
let exitMock;
|
||||
|
||||
// Mock console
|
||||
const originalError = console.error;
|
||||
let errorMock;
|
||||
const originalLog = console.log;
|
||||
let logMock;
|
||||
|
||||
describe('main dispatch', () => {
|
||||
let main;
|
||||
|
||||
before(async () => {
|
||||
exitMock = mock.fn();
|
||||
process.exit = exitMock;
|
||||
|
||||
errorMock = mock.fn();
|
||||
console.error = errorMock;
|
||||
|
||||
logMock = mock.fn();
|
||||
console.log = logMock;
|
||||
|
||||
// We import main
|
||||
// Since we cannot mock modules easily in this environment (mock.module missing),
|
||||
// we will rely on testing the error paths of the handlers, which don't trigger side effects.
|
||||
const mod = await import('../src/lib.mjs');
|
||||
main = mod.main;
|
||||
});
|
||||
|
||||
after(() => {
|
||||
process.exit = originalExit;
|
||||
console.error = originalError;
|
||||
console.log = originalLog;
|
||||
mock.reset();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
exitMock.mock.resetCalls();
|
||||
errorMock.mock.resetCalls();
|
||||
logMock.mock.resetCalls();
|
||||
});
|
||||
|
||||
it('should exit with code 1 if no command provided', async () => {
|
||||
await main([]);
|
||||
|
||||
assert.strictEqual(exitMock.mock.callCount(), 1);
|
||||
assert.strictEqual(exitMock.mock.calls[0].arguments[0], 1);
|
||||
});
|
||||
|
||||
it('should exit with code 1 if unknown command provided', async () => {
|
||||
await main(['unknown']);
|
||||
|
||||
assert.strictEqual(exitMock.mock.callCount(), 1);
|
||||
assert.strictEqual(exitMock.mock.calls[0].arguments[0], 1);
|
||||
const unknownMsg = errorMock.mock.calls.find(c => c.arguments[0].includes('Unknown command'));
|
||||
assert.ok(unknownMsg);
|
||||
});
|
||||
|
||||
it('should exit with code 1 if check command misses cadence', async () => {
|
||||
await main(['check']);
|
||||
|
||||
assert.strictEqual(exitMock.mock.callCount(), 1);
|
||||
assert.strictEqual(exitMock.mock.calls[0].arguments[0], 1);
|
||||
const missingMsg = errorMock.mock.calls.find(c => c.arguments[0].includes('required for check'));
|
||||
assert.ok(missingMsg);
|
||||
});
|
||||
|
||||
it('should exit with code 1 if lock command misses agent', async () => {
|
||||
await main(['lock']);
|
||||
|
||||
assert.strictEqual(exitMock.mock.callCount(), 1);
|
||||
assert.strictEqual(exitMock.mock.calls[0].arguments[0], 1);
|
||||
const missingMsg = errorMock.mock.calls.find(c => c.arguments[0].includes('required for lock'));
|
||||
assert.ok(missingMsg);
|
||||
});
|
||||
|
||||
it('should exit with code 1 if complete command misses agent', async () => {
|
||||
await main(['complete']);
|
||||
|
||||
assert.strictEqual(exitMock.mock.callCount(), 1);
|
||||
assert.strictEqual(exitMock.mock.calls[0].arguments[0], 1);
|
||||
const missingMsg = errorMock.mock.calls.find(c => c.arguments[0].includes('required for complete'));
|
||||
assert.ok(missingMsg);
|
||||
});
|
||||
|
||||
it('should exit with code 1 if proposal command misses subcommand', async () => {
|
||||
await main(['proposal']);
|
||||
|
||||
assert.strictEqual(exitMock.mock.callCount(), 1);
|
||||
assert.strictEqual(exitMock.mock.calls[0].arguments[0], 1);
|
||||
const missingMsg = errorMock.mock.calls.find(c => c.arguments[0].includes('Missing subcommand'));
|
||||
assert.ok(missingMsg);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import { withTimeout } from '../src/utils.mjs';
|
||||
|
||||
describe('Async Utilities', () => {
|
||||
describe('withTimeout', () => {
|
||||
it('resolves when promise completes before timeout', async () => {
|
||||
const result = await withTimeout(
|
||||
new Promise((resolve) => setTimeout(() => resolve('success'), 10)),
|
||||
200,
|
||||
'Timed out'
|
||||
);
|
||||
assert.strictEqual(result, 'success');
|
||||
});
|
||||
|
||||
it('rejects when promise times out', async () => {
|
||||
try {
|
||||
await withTimeout(
|
||||
new Promise((resolve) => setTimeout(() => resolve('success'), 200)),
|
||||
10,
|
||||
'Operation timed out'
|
||||
);
|
||||
assert.fail('Should have rejected');
|
||||
} catch (err) {
|
||||
assert.strictEqual(err.message, 'Operation timed out');
|
||||
}
|
||||
});
|
||||
|
||||
it('clears timeout on success', async () => {
|
||||
const result = await withTimeout(
|
||||
Promise.resolve('fast'),
|
||||
1000,
|
||||
'timeout'
|
||||
);
|
||||
assert.strictEqual(result, 'fast');
|
||||
});
|
||||
|
||||
it('propagates original promise rejection', async () => {
|
||||
try {
|
||||
await withTimeout(
|
||||
Promise.reject(new Error('Original error')),
|
||||
100,
|
||||
'Timeout'
|
||||
);
|
||||
assert.fail('Should have rejected');
|
||||
} catch (err) {
|
||||
assert.strictEqual(err.message, 'Original error');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
+18
-1
@@ -2,7 +2,7 @@ import { describe, it, after } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { getIsoWeekStr, todayDateStr, nowUnix, ensureDir, detectPlatform } from '../src/utils.mjs';
|
||||
import { getIsoWeekStr, todayDateStr, nowUnix, ensureDir, detectPlatform, mergeRelayList } from '../src/utils.mjs';
|
||||
|
||||
describe('Date Utilities', () => {
|
||||
describe('todayDateStr', () => {
|
||||
@@ -59,6 +59,23 @@ describe('Date Utilities', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Relay Utilities', () => {
|
||||
describe('mergeRelayList', () => {
|
||||
it('merges two lists of relays without duplicates', () => {
|
||||
const primary = ['wss://relay1.com', 'wss://relay2.com'];
|
||||
const fallback = ['wss://relay2.com', 'wss://relay3.com'];
|
||||
const merged = mergeRelayList(primary, fallback);
|
||||
assert.deepStrictEqual(merged, ['wss://relay1.com', 'wss://relay2.com', 'wss://relay3.com']);
|
||||
});
|
||||
|
||||
it('handles empty lists', () => {
|
||||
assert.deepStrictEqual(mergeRelayList([], []), []);
|
||||
assert.deepStrictEqual(mergeRelayList(['wss://relay1.com'], []), ['wss://relay1.com']);
|
||||
assert.deepStrictEqual(mergeRelayList([], ['wss://relay1.com']), ['wss://relay1.com']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Platform Utilities', () => {
|
||||
describe('detectPlatform', () => {
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user