From 00b103cf6e47fdfd3c7e541db7df888a3aaba59d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Feb 2026 05:03:26 +0000 Subject: [PATCH 1/3] chore(agents): claim event-schema-agent weekly task https://claude.ai/code/session_018HLVbfKYtfPMWcSMaC1W7N --- .../2026-02-13_05-03-10_event-schema-agent_started.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 docs/agents/task-logs/weekly/2026-02-13_05-03-10_event-schema-agent_started.md diff --git a/docs/agents/task-logs/weekly/2026-02-13_05-03-10_event-schema-agent_started.md b/docs/agents/task-logs/weekly/2026-02-13_05-03-10_event-schema-agent_started.md new file mode 100644 index 00000000..e1c2be4f --- /dev/null +++ b/docs/agents/task-logs/weekly/2026-02-13_05-03-10_event-schema-agent_started.md @@ -0,0 +1,8 @@ +# Agent Task Log Entry + +- **Date:** 2026-02-13 +- **Agent:** event-schema-agent +- **Prompt:** bitvid-event-schema-agent.md +- **Status:** started +- **Branch:** claude/prevent-duplicate-scheduler-work-dj6XE +- **Summary:** Task claimed — execution beginning From 0e46902ed02bbf5c46f3f7300fbb070705ba1f6f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Feb 2026 05:12:52 +0000 Subject: [PATCH 2/3] =?UTF-8?q?chore(agents):=20run=20event-schema-agent?= =?UTF-8?q?=20weekly=20task=20=E2=80=94=20enhance=20validator=20with=20CLI?= =?UTF-8?q?,=20reporting,=20and=20registry=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhanced scripts/agent/validate-events.mjs with: - CLI options: --dry-run, --out=, --only= - JSON report output (artifacts/validate-events-YYYYMMDD.json) - sanitizeAdditionalTags test cases (3 tests) - Schema registry verification via getAllNostrEventSchemas (35 checks) - Runtime construction site mapping (29 call sites across codebase) - Structured report format per agent prompt spec All 66 validation checks pass. No schema failures found. https://claude.ai/code/session_018HLVbfKYtfPMWcSMaC1W7N --- ...3_05-12-12_event-schema-agent_completed.md | 8 + scripts/agent/validate-events.mjs | 932 ++++++++++++------ 2 files changed, 633 insertions(+), 307 deletions(-) create mode 100644 docs/agents/task-logs/weekly/2026-02-13_05-12-12_event-schema-agent_completed.md diff --git a/docs/agents/task-logs/weekly/2026-02-13_05-12-12_event-schema-agent_completed.md b/docs/agents/task-logs/weekly/2026-02-13_05-12-12_event-schema-agent_completed.md new file mode 100644 index 00000000..75261a50 --- /dev/null +++ b/docs/agents/task-logs/weekly/2026-02-13_05-12-12_event-schema-agent_completed.md @@ -0,0 +1,8 @@ +# Agent Task Log Entry + +- **Date:** 2026-02-13 +- **Agent:** event-schema-agent +- **Prompt:** bitvid-event-schema-agent.md +- **Status:** completed +- **Branch:** claude/prevent-duplicate-scheduler-work-dj6XE +- **Summary:** Enhanced validate-events.mjs with CLI options (--dry-run, --out, --only), JSON report output, sanitizer tests, schema registry verification, and runtime construction site mapping. All 66 checks pass (28 builders, 3 sanitizers, 35 registry). No schema failures found. diff --git a/scripts/agent/validate-events.mjs b/scripts/agent/validate-events.mjs index add4c996..456cc69b 100644 --- a/scripts/agent/validate-events.mjs +++ b/scripts/agent/validate-events.mjs @@ -1,4 +1,6 @@ import crypto from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; // Polyfills for browser environment class MockWebSocket { @@ -70,345 +72,661 @@ import { buildSealEvent, buildChatMessageEvent, getNostrEventSchema, + getAllNostrEventSchemas, sanitizeAdditionalTags } from '../../js/nostrEventSchemas.js'; import { buildNip71VideoEvent } from '../../js/nostr/nip71.js'; +// --- CLI argument parsing --- +function parseArgs(argv) { + const args = { dryRun: false, out: null, only: null }; + for (const arg of argv.slice(2)) { + if (arg === '--dry-run') { + args.dryRun = true; + } else if (arg.startsWith('--out=')) { + args.out = arg.slice('--out='.length); + } else if (arg.startsWith('--only=')) { + args.only = arg.slice('--only='.length); + } + } + return args; +} + +const cliArgs = parseArgs(process.argv); + const TEST_PUBKEY = '0000000000000000000000000000000000000000000000000000000000000001'; const TEST_TIMESTAMP = 1672531200; // 2023-01-01 -function runTest(name, builderFn, input, expectedType, additionalCheck) { - console.log(`\nTesting: ${name}`); +// Known runtime construction sites (from codebase scan) +const RUNTIME_SITES = [ + { builder: 'buildVideoPostEvent', file: 'js/nostr/videoPayloadBuilder.js', line: 172, context: 'normalizeVideoNotePayload()' }, + { builder: 'buildVideoPostEvent', file: 'js/nostr/videoPayloadBuilder.js', line: 462, context: 'updateVideoPayloadVersion()' }, + { builder: 'buildVideoPostEvent', file: 'js/nostr/publishHelpers.js', line: 1405, context: 'buildVideoEventFromPayload()' }, + { builder: 'buildVideoMirrorEvent', file: 'js/nostr/publishHelpers.js', line: 1164, context: 'mirrorVideoEvent()' }, + { builder: 'buildNip71VideoEvent', file: 'js/nostr/videoPublisher.js', line: 187, context: 'publishNip71Video()' }, + { builder: 'buildProfileMetadataEvent', file: 'js/ui/profileModalController.js', line: 5081, context: 'profile save handler' }, + { builder: 'buildShareEvent', file: 'js/ui/shareNostrController.js', line: 166, context: 'handleShareEvent()' }, + { builder: 'buildRepostEvent', file: 'js/nostr/publishHelpers.js', line: 930, context: 'createRepostEvent()' }, + { builder: 'buildCommentEvent', file: 'js/nostr/commentEvents.js', line: 233, context: 'publishCommentEvent()' }, + { builder: 'buildReactionEvent', file: 'js/nostr/reactionEvents.js', line: 414, context: 'publishReactionEvent()' }, + { builder: 'buildViewEvent', file: 'js/nostr/viewEvents.js', line: 782, context: 'recordVideoViewWithTracking()' }, + { builder: 'buildWatchHistoryEvent', file: 'js/nostr/watchHistory.js', line: 1445, context: 'publishRecords()' }, + { builder: 'buildChatMessageEvent', file: 'js/nostr/client.js', line: 1719, context: 'sendDirectMessage()' }, + { builder: 'buildDmAttachmentEvent', file: 'js/nostr/client.js', line: 1744, context: 'sendDirectMessage()' }, + { builder: 'buildSealEvent', file: 'js/nostr/client.js', line: 1763, context: 'NIP-59 seal for gift wrap' }, + { builder: 'buildGiftWrapEvent', file: 'js/nostr/client.js', line: 1794, context: 'NIP-59 gift wrap for DM' }, + { builder: 'buildLegacyDirectMessageEvent', file: 'js/nostr/client.js', line: 2013, context: 'NIP-04 legacy DM' }, + { builder: 'buildDmReadReceiptEvent', file: 'js/nostr/dmSignalEvents.js', line: 75, context: 'DM read receipt signal' }, + { builder: 'buildDmTypingIndicatorEvent', file: 'js/nostr/dmSignalEvents.js', line: 216, context: 'DM typing indicator signal' }, + { builder: 'buildMuteListEvent', file: 'js/userBlocks.js', line: 2224, context: 'updateUserBlocks()' }, + { builder: 'buildAdminListEvent', file: 'js/adminListStore.js', line: 700, context: 'buildListEvent()' }, + { builder: 'buildReportEvent', file: 'js/services/moderationService.js', line: 2449, context: 'publishReport()' }, + { builder: 'buildSubscriptionListEvent', file: 'js/subscriptions.js', line: 1456, context: 'publishing subscription list' }, + { builder: 'buildHashtagPreferenceEvent', file: 'js/services/hashtagPreferencesService.js', line: 1365, context: 'buildEvent()' }, + { builder: 'buildRelayListEvent', file: 'js/relayManager.js', line: 699, context: 'updateRelayList()' }, + { builder: 'buildDmRelayListEvent', file: 'js/app.js', line: 2839, context: 'DM relay sync' }, + { builder: 'buildZapRequestEvent', file: 'js/payments/zapRequests.js', line: 88, context: 'createZapRequest()' }, + { builder: 'buildDeletionEvent', file: 'js/nostr/client.js', line: 3060, context: 'deletePublishedVideos()' }, + { builder: 'buildHttpAuthEvent', file: 'js/nostr/client.js', line: 0, context: 'HTTP auth flow (if used)' }, +]; + +// --- Test case definitions --- +const TEST_CASES = [ + { + name: 'buildVideoPostEvent (Real Usage - Auto Storage Pointer)', + builder: 'buildVideoPostEvent', + builderFn: buildVideoPostEvent, + input: { + pubkey: TEST_PUBKEY, + created_at: TEST_TIMESTAMP, + dTagValue: 'test-video-id-123', + content: { + version: 3, + title: 'Real World Video', + videoRootId: 'test-video-id-123', + url: 'https://example.com/video.mp4', + mode: 'live', + deleted: false, + isPrivate: false, + isNsfw: false, + isForKids: false, + enableComments: true + }, + additionalTags: [['t', 'art']] + }, + expectedType: NOTE_TYPES.VIDEO_POST, + additionalCheck: (event) => { + const sTag = event.tags.find(t => t[0] === 's'); + if (!sTag) throw new Error("Missing 's' tag (Storage Pointer) which should have been auto-generated from URL"); + if (!sTag[1].startsWith('url:')) throw new Error(`'s' tag should start with 'url:', got ${sTag[1]}`); + } + }, + { + name: 'buildRelayListEvent (Real Usage)', + builder: 'buildRelayListEvent', + builderFn: buildRelayListEvent, + input: { + pubkey: TEST_PUBKEY, + created_at: TEST_TIMESTAMP, + relays: [ + { url: 'wss://relay.damus.io', mode: 'read' }, + { url: 'wss://nos.lol', mode: 'write' }, + { url: 'wss://relay.primal.net', mode: 'both' } + ] + }, + expectedType: NOTE_TYPES.RELAY_LIST, + additionalCheck: (event) => { + const rTags = event.tags.filter(t => t[0] === 'r'); + if (rTags.length !== 3) throw new Error(`Expected 3 'r' tags, got ${rTags.length}`); + const damus = rTags.find(t => t[1] === 'wss://relay.damus.io'); + if (!damus || damus[2] !== 'read') throw new Error("Damus relay missing or incorrect marker"); + } + }, + { + name: 'buildHashtagPreferenceEvent (Real Usage - Encrypted)', + builder: 'buildHashtagPreferenceEvent', + builderFn: buildHashtagPreferenceEvent, + input: { + pubkey: TEST_PUBKEY, + created_at: TEST_TIMESTAMP, + content: "nip44:ciphertext-blob-simulated", + additionalTags: [['encrypted', 'nip44']] + }, + expectedType: NOTE_TYPES.HASHTAG_PREFERENCES, + additionalCheck: (event) => { + if (event.content !== "nip44:ciphertext-blob-simulated") throw new Error("Content mismatch"); + const dTag = event.tags.find(t => t[0] === 'd'); + if (!dTag || dTag[1] !== 'bitvid:tag-preferences') throw new Error("Incorrect 'd' tag identifier"); + } + }, + { + name: 'buildHttpAuthEvent', + builder: 'buildHttpAuthEvent', + builderFn: buildHttpAuthEvent, + input: { + pubkey: TEST_PUBKEY, + created_at: TEST_TIMESTAMP, + url: 'https://api.example.com/auth', + method: 'POST', + payload: 'sha256-hash-of-body' + }, + expectedType: NOTE_TYPES.HTTP_AUTH + }, + { + name: 'buildReportEvent', + builder: 'buildReportEvent', + builderFn: buildReportEvent, + input: { + pubkey: TEST_PUBKEY, + created_at: TEST_TIMESTAMP, + eventId: 'e'.repeat(64), + reportType: 'spam', + content: 'This is spam' + }, + expectedType: NOTE_TYPES.REPORT + }, + { + name: 'buildVideoMirrorEvent', + builder: 'buildVideoMirrorEvent', + builderFn: buildVideoMirrorEvent, + input: { + pubkey: TEST_PUBKEY, + created_at: TEST_TIMESTAMP, + content: 'Mirror description' + }, + expectedType: NOTE_TYPES.VIDEO_MIRROR + }, + { + name: 'buildRepostEvent', + builder: 'buildRepostEvent', + builderFn: buildRepostEvent, + input: { + pubkey: TEST_PUBKEY, + created_at: TEST_TIMESTAMP, + eventId: 'e'.repeat(64), + eventRelay: 'wss://relay.example.com' + }, + expectedType: NOTE_TYPES.REPOST + }, + { + name: 'buildShareEvent', + builder: 'buildShareEvent', + builderFn: buildShareEvent, + input: { + pubkey: TEST_PUBKEY, + created_at: TEST_TIMESTAMP, + content: 'Check this out', + video: { id: 'e'.repeat(64), pubkey: TEST_PUBKEY } + }, + expectedType: NOTE_TYPES.SHARE + }, + { + name: 'buildDmRelayListEvent', + builder: 'buildDmRelayListEvent', + builderFn: buildDmRelayListEvent, + input: { + pubkey: TEST_PUBKEY, + created_at: TEST_TIMESTAMP, + relays: ['wss://dm.relay.com'] + }, + expectedType: NOTE_TYPES.DM_RELAY_LIST + }, + { + name: 'buildProfileMetadataEvent', + builder: 'buildProfileMetadataEvent', + builderFn: buildProfileMetadataEvent, + input: { + pubkey: TEST_PUBKEY, + created_at: TEST_TIMESTAMP, + metadata: { name: 'Alice', about: 'Test user' } + }, + expectedType: NOTE_TYPES.PROFILE_METADATA + }, + { + name: 'buildMuteListEvent', + builder: 'buildMuteListEvent', + builderFn: buildMuteListEvent, + input: { + pubkey: TEST_PUBKEY, + created_at: TEST_TIMESTAMP, + pTags: [TEST_PUBKEY] + }, + expectedType: NOTE_TYPES.MUTE_LIST + }, + { + name: 'buildDeletionEvent', + builder: 'buildDeletionEvent', + builderFn: buildDeletionEvent, + input: { + pubkey: TEST_PUBKEY, + created_at: TEST_TIMESTAMP, + eventIds: ['e'.repeat(64)], + reason: 'Mistake' + }, + expectedType: NOTE_TYPES.DELETION + }, + { + name: 'buildLegacyDirectMessageEvent', + builder: 'buildLegacyDirectMessageEvent', + builderFn: buildLegacyDirectMessageEvent, + input: { + pubkey: TEST_PUBKEY, + created_at: TEST_TIMESTAMP, + recipientPubkey: TEST_PUBKEY, + ciphertext: 'encrypted-stuff' + }, + expectedType: NOTE_TYPES.LEGACY_DM + }, + { + name: 'buildDmAttachmentEvent', + builder: 'buildDmAttachmentEvent', + builderFn: buildDmAttachmentEvent, + input: { + pubkey: TEST_PUBKEY, + created_at: TEST_TIMESTAMP, + recipientPubkey: TEST_PUBKEY, + attachment: { url: 'https://example.com/file.jpg', type: 'image/jpeg' } + }, + expectedType: NOTE_TYPES.DM_ATTACHMENT + }, + { + name: 'buildDmReadReceiptEvent', + builder: 'buildDmReadReceiptEvent', + builderFn: buildDmReadReceiptEvent, + input: { + pubkey: TEST_PUBKEY, + created_at: TEST_TIMESTAMP, + recipientPubkey: TEST_PUBKEY, + eventId: 'e'.repeat(64) + }, + expectedType: NOTE_TYPES.DM_READ_RECEIPT + }, + { + name: 'buildDmTypingIndicatorEvent', + builder: 'buildDmTypingIndicatorEvent', + builderFn: buildDmTypingIndicatorEvent, + input: { + pubkey: TEST_PUBKEY, + created_at: TEST_TIMESTAMP, + recipientPubkey: TEST_PUBKEY + }, + expectedType: NOTE_TYPES.DM_TYPING + }, + { + name: 'buildViewEvent', + builder: 'buildViewEvent', + builderFn: buildViewEvent, + input: { + pubkey: TEST_PUBKEY, + created_at: TEST_TIMESTAMP, + pointerValue: 'test-pointer', + pointerTag: ['a', 'test-pointer'] + }, + expectedType: NOTE_TYPES.VIEW_EVENT + }, + { + name: 'buildZapRequestEvent', + builder: 'buildZapRequestEvent', + builderFn: buildZapRequestEvent, + input: { + pubkey: TEST_PUBKEY, + created_at: TEST_TIMESTAMP, + recipientPubkey: TEST_PUBKEY, + amountSats: 100, + relays: ['wss://relay.zap.com'] + }, + expectedType: NOTE_TYPES.ZAP_REQUEST + }, + { + name: 'buildReactionEvent', + builder: 'buildReactionEvent', + builderFn: buildReactionEvent, + input: { + pubkey: TEST_PUBKEY, + created_at: TEST_TIMESTAMP, + pointerTag: ['e', 'e'.repeat(64)], + content: '+' + }, + expectedType: NOTE_TYPES.VIDEO_REACTION + }, + { + name: 'buildCommentEvent', + builder: 'buildCommentEvent', + builderFn: buildCommentEvent, + input: { + pubkey: TEST_PUBKEY, + created_at: TEST_TIMESTAMP, + videoEventId: 'e'.repeat(64), + content: 'Nice video!' + }, + expectedType: NOTE_TYPES.VIDEO_COMMENT + }, + { + name: 'buildWatchHistoryEvent', + builder: 'buildWatchHistoryEvent', + builderFn: buildWatchHistoryEvent, + input: { + pubkey: TEST_PUBKEY, + created_at: TEST_TIMESTAMP, + monthIdentifier: '2023-01', + content: JSON.stringify({ ['e'.repeat(64)]: 123456 }) + }, + expectedType: NOTE_TYPES.WATCH_HISTORY + }, + { + name: 'buildSubscriptionListEvent', + builder: 'buildSubscriptionListEvent', + builderFn: buildSubscriptionListEvent, + input: { + pubkey: TEST_PUBKEY, + created_at: TEST_TIMESTAMP + }, + expectedType: NOTE_TYPES.SUBSCRIPTION_LIST + }, + { + name: 'buildBlockListEvent', + builder: 'buildBlockListEvent', + builderFn: buildBlockListEvent, + input: { + pubkey: TEST_PUBKEY, + created_at: TEST_TIMESTAMP + }, + expectedType: NOTE_TYPES.USER_BLOCK_LIST + }, + { + name: 'buildAdminListEvent (moderation)', + builder: 'buildAdminListEvent', + builderFn: (i) => buildAdminListEvent(ADMIN_LIST_IDENTIFIERS.moderation, i), + input: { + pubkey: TEST_PUBKEY, + created_at: TEST_TIMESTAMP, + hexPubkeys: [TEST_PUBKEY] + }, + expectedType: NOTE_TYPES.ADMIN_MODERATION_LIST + }, + { + name: 'buildNip71VideoEvent', + builder: 'buildNip71VideoEvent', + builderFn: buildNip71VideoEvent, + input: { + pubkey: TEST_PUBKEY, + title: 'NIP-71 Video', + metadata: { + kind: 21, + title: 'NIP-71 Video', + publishedAt: TEST_TIMESTAMP, + hashtags: ['test'] + }, + pointerIdentifiers: { videoRootId: 'test-root' } + }, + expectedType: NOTE_TYPES.NIP71_VIDEO + }, + { + name: 'buildGiftWrapEvent', + builder: 'buildGiftWrapEvent', + builderFn: buildGiftWrapEvent, + input: { + pubkey: TEST_PUBKEY, + created_at: TEST_TIMESTAMP, + recipientPubkey: TEST_PUBKEY, + ciphertext: 'encrypted-seal', + relayHint: 'wss://relay.example.com' + }, + expectedType: NOTE_TYPES.GIFT_WRAP + }, + { + name: 'buildSealEvent', + builder: 'buildSealEvent', + builderFn: buildSealEvent, + input: { + pubkey: TEST_PUBKEY, + created_at: TEST_TIMESTAMP, + ciphertext: 'encrypted-rumor' + }, + expectedType: NOTE_TYPES.SEAL + }, + { + name: 'buildChatMessageEvent', + builder: 'buildChatMessageEvent', + builderFn: buildChatMessageEvent, + input: { + pubkey: TEST_PUBKEY, + created_at: TEST_TIMESTAMP, + recipientPubkey: TEST_PUBKEY, + content: 'Hello World' + }, + expectedType: NOTE_TYPES.CHAT_MESSAGE + }, +]; + +// --- Sanitizer test cases --- +const SANITIZER_TESTS = [ + { + name: 'sanitizeAdditionalTags (valid tags)', + input: [['t', 'video'], ['p', TEST_PUBKEY]], + check: (result) => { + if (!Array.isArray(result) || result.length !== 2) throw new Error(`Expected 2 tags, got ${result.length}`); + if (result[0][0] !== 't' || result[0][1] !== 'video') throw new Error('First tag mismatch'); + } + }, + { + name: 'sanitizeAdditionalTags (filters invalid)', + input: [['', 'value'], [123, 'value'], ['valid', 'ok']], + check: (result) => { + if (result.length !== 1) throw new Error(`Expected 1 valid tag after filtering, got ${result.length}`); + if (result[0][0] !== 'valid') throw new Error('Surviving tag should be "valid"'); + } + }, + { + name: 'sanitizeAdditionalTags (empty input)', + input: [], + check: (result) => { + if (!Array.isArray(result) || result.length !== 0) throw new Error('Expected empty array'); + } + }, +]; + +function runTest(testCase) { + const { name, builderFn, input, expectedType, additionalCheck } = testCase; + const result = { + builder: testCase.builder, + name, + constructed_by: `validate-events.mjs (test harness)`, + input: '(redacted — see test case definition)', + event: null, + validation: { status: 'PASS', failures: [] }, + suggested_fix: null, + runtime_sites: RUNTIME_SITES.filter(s => s.builder === testCase.builder), + }; + try { const event = builderFn(input); if (!event) { - console.error(`❌ Builder returned null or undefined`); - return false; + result.validation.status = 'FAIL'; + result.validation.failures.push({ path: 'builder', message: 'Builder returned null or undefined' }); + return result; } + result.event = { kind: event.kind, tags_count: event.tags?.length, content_length: event.content?.length }; + const { valid, errors } = validateEventStructure(expectedType, event); - if (valid) { - if (additionalCheck) { - try { - additionalCheck(event); - } catch (checkError) { - console.error(`❌ Additional check failed: ${checkError.message}`); - return false; - } + if (!valid) { + result.validation.status = 'FAIL'; + result.validation.failures = errors.map(msg => ({ path: 'schema', message: msg })); + return result; + } + + if (additionalCheck) { + try { + additionalCheck(event); + } catch (checkError) { + result.validation.status = 'FAIL'; + result.validation.failures.push({ path: 'additional_check', message: checkError.message }); + return result; } - console.log(`✅ Valid ${expectedType} (Kind: ${event.kind})`); - return true; - } else { - console.error(`❌ Invalid ${expectedType}:`); - errors.forEach(err => console.error(` - ${err}`)); - return false; } } catch (error) { - console.error(`❌ Exception during build:`, error); - return false; + result.validation.status = 'FAIL'; + result.validation.failures.push({ path: 'exception', message: error.message }); } + + return result; +} + +function runSanitizerTest(testCase) { + const result = { + builder: 'sanitizeAdditionalTags', + name: testCase.name, + validation: { status: 'PASS', failures: [] }, + }; + + try { + const output = sanitizeAdditionalTags(testCase.input); + testCase.check(output); + } catch (error) { + result.validation.status = 'FAIL'; + result.validation.failures.push({ path: 'sanitizer', message: error.message }); + } + + return result; +} + +function verifySchemaRegistry() { + const results = []; + const allSchemas = getAllNostrEventSchemas(); + const schemaKeys = Object.keys(allSchemas); + const noteTypeValues = Object.values(NOTE_TYPES); + + // Check that every NOTE_TYPE has a corresponding schema + for (const noteType of noteTypeValues) { + const schema = getNostrEventSchema(noteType); + const result = { + builder: 'getNostrEventSchema', + name: `Schema registry: ${noteType}`, + validation: { status: 'PASS', failures: [] }, + }; + if (!schema) { + result.validation.status = 'FAIL'; + result.validation.failures.push({ path: 'registry', message: `No schema found for NOTE_TYPE: ${noteType}` }); + } else if (typeof schema.kind !== 'number') { + result.validation.status = 'FAIL'; + result.validation.failures.push({ path: 'registry', message: `Schema for ${noteType} has non-numeric kind: ${schema.kind}` }); + } + results.push(result); + } + + // Verify getAllNostrEventSchemas returns all types + const registryResult = { + builder: 'getAllNostrEventSchemas', + name: 'Schema registry completeness', + validation: { status: 'PASS', failures: [] }, + }; + if (schemaKeys.length < noteTypeValues.length) { + registryResult.validation.status = 'FAIL'; + registryResult.validation.failures.push({ + path: 'registry', + message: `getAllNostrEventSchemas returned ${schemaKeys.length} schemas but NOTE_TYPES has ${noteTypeValues.length} values` + }); + } + results.push(registryResult); + + return results; } async function main() { + const testCases = cliArgs.only + ? TEST_CASES.filter(tc => tc.builder === cliArgs.only) + : TEST_CASES; + + if (cliArgs.only && testCases.length === 0) { + console.error(`No test cases matched --only=${cliArgs.only}`); + process.exit(1); + } + console.log('Starting Event Schema Validation...'); + if (cliArgs.only) console.log(` Filtered to: ${cliArgs.only}`); + if (cliArgs.dryRun) console.log(' (dry-run mode — no report file will be written)'); + + const report = { + timestamp: new Date().toISOString(), + cli_args: { dry_run: cliArgs.dryRun, out: cliArgs.out, only: cliArgs.only }, + builder_results: [], + sanitizer_results: [], + schema_registry_results: [], + runtime_sites: RUNTIME_SITES, + summary: { total: 0, passed: 0, failed: 0 }, + }; + + // Run builder tests let failures = 0; + for (const tc of testCases) { + const result = runTest(tc); + report.builder_results.push(result); + if (result.validation.status === 'PASS') { + console.log(`\n✅ ${result.name} (Kind: ${result.event?.kind})`); + } else { + failures++; + console.error(`\n❌ ${result.name}`); + result.validation.failures.forEach(f => console.error(` - [${f.path}] ${f.message}`)); + } + } - // --- Real-world usage patterns --- + // Run sanitizer tests (unless --only filters them out) + if (!cliArgs.only || cliArgs.only === 'sanitizeAdditionalTags') { + for (const st of SANITIZER_TESTS) { + const result = runSanitizerTest(st); + report.sanitizer_results.push(result); + if (result.validation.status === 'PASS') { + console.log(`\n✅ ${result.name}`); + } else { + failures++; + console.error(`\n❌ ${result.name}`); + result.validation.failures.forEach(f => console.error(` - [${f.path}] ${f.message}`)); + } + } + } - // 1. Video Post (mimicking js/nostr/client.js publishVideo) - // Testing auto-generation of 's' tag from URL - const videoInputReal = { - pubkey: TEST_PUBKEY, - created_at: TEST_TIMESTAMP, - dTagValue: 'test-video-id-123', - content: { - version: 3, - title: 'Real World Video', - videoRootId: 'test-video-id-123', - url: 'https://example.com/video.mp4', - mode: 'live', - deleted: false, - isPrivate: false, - isNsfw: false, - isForKids: false, - enableComments: true - }, - // No explicit 's' tag passed, should be generated - additionalTags: [['t', 'art']] - }; + // Run schema registry checks (unless --only filters them out) + if (!cliArgs.only) { + const registryResults = verifySchemaRegistry(); + report.schema_registry_results = registryResults; + for (const rr of registryResults) { + if (rr.validation.status === 'PASS') { + // Don't spam console for per-type registry checks; just count + } else { + failures++; + console.error(`\n❌ ${rr.name}`); + rr.validation.failures.forEach(f => console.error(` - [${f.path}] ${f.message}`)); + } + } + const regPassed = registryResults.filter(r => r.validation.status === 'PASS').length; + const regFailed = registryResults.filter(r => r.validation.status !== 'PASS').length; + console.log(`\n📋 Schema registry: ${regPassed} passed, ${regFailed} failed`); + } - if (!runTest('buildVideoPostEvent (Real Usage - Auto Storage Pointer)', buildVideoPostEvent, videoInputReal, NOTE_TYPES.VIDEO_POST, (event) => { - const sTag = event.tags.find(t => t[0] === 's'); - if (!sTag) throw new Error("Missing 's' tag (Storage Pointer) which should have been auto-generated from URL"); - if (!sTag[1].startsWith('url:')) throw new Error(`'s' tag should start with 'url:', got ${sTag[1]}`); - })) failures++; - - // 2. Relay List (mimicking js/relayManager.js) - const relayListInputReal = { - pubkey: TEST_PUBKEY, - created_at: TEST_TIMESTAMP, - relays: [ - { url: 'wss://relay.damus.io', mode: 'read' }, - { url: 'wss://nos.lol', mode: 'write' }, - { url: 'wss://relay.primal.net', mode: 'both' } - ] - }; - if (!runTest('buildRelayListEvent (Real Usage)', buildRelayListEvent, relayListInputReal, NOTE_TYPES.RELAY_LIST, (event) => { - const rTags = event.tags.filter(t => t[0] === 'r'); - if (rTags.length !== 3) throw new Error(`Expected 3 'r' tags, got ${rTags.length}`); - const damus = rTags.find(t => t[1] === 'wss://relay.damus.io'); - if (!damus || damus[2] !== 'read') throw new Error("Damus relay missing or incorrect marker"); - })) failures++; - - // 3. Hashtag Preferences (mimicking js/services/hashtagPreferencesService.js) - // Content is encrypted string (ciphertext) - const hashtagInputReal = { - pubkey: TEST_PUBKEY, - created_at: TEST_TIMESTAMP, - content: "nip44:ciphertext-blob-simulated", - // Service adds encrypted tag - additionalTags: [['encrypted', 'nip44']] - }; - if (!runTest('buildHashtagPreferenceEvent (Real Usage - Encrypted)', buildHashtagPreferenceEvent, hashtagInputReal, NOTE_TYPES.HASHTAG_PREFERENCES, (event) => { - if (event.content !== "nip44:ciphertext-blob-simulated") throw new Error("Content mismtach"); - const dTag = event.tags.find(t => t[0] === 'd'); - if (!dTag || dTag[1] !== 'bitvid:tag-preferences') throw new Error("Incorrect 'd' tag identifier"); - })) failures++; - - - // --- Standard Checks --- - - // 4. HTTP Auth - const httpAuthInput = { - pubkey: TEST_PUBKEY, - created_at: TEST_TIMESTAMP, - url: 'https://api.example.com/auth', - method: 'POST', - payload: 'sha256-hash-of-body' - }; - if (!runTest('buildHttpAuthEvent', buildHttpAuthEvent, httpAuthInput, NOTE_TYPES.HTTP_AUTH)) failures++; - - // 5. Report - const reportInput = { - pubkey: TEST_PUBKEY, - created_at: TEST_TIMESTAMP, - eventId: 'e'.repeat(64), - reportType: 'spam', - content: 'This is spam' - }; - if (!runTest('buildReportEvent', buildReportEvent, reportInput, NOTE_TYPES.REPORT)) failures++; - - // 6. Video Mirror (NIP-94) - const mirrorInput = { - pubkey: TEST_PUBKEY, - created_at: TEST_TIMESTAMP, - content: 'Mirror description' - }; - if (!runTest('buildVideoMirrorEvent', buildVideoMirrorEvent, mirrorInput, NOTE_TYPES.VIDEO_MIRROR)) failures++; - - // 7. Repost - const repostInput = { - pubkey: TEST_PUBKEY, - created_at: TEST_TIMESTAMP, - eventId: 'e'.repeat(64), - eventRelay: 'wss://relay.example.com' - }; - if (!runTest('buildRepostEvent', buildRepostEvent, repostInput, NOTE_TYPES.REPOST)) failures++; - - // 8. Share - const shareInput = { - pubkey: TEST_PUBKEY, - created_at: TEST_TIMESTAMP, - content: 'Check this out', - video: { id: 'e'.repeat(64), pubkey: TEST_PUBKEY } - }; - if (!runTest('buildShareEvent', buildShareEvent, shareInput, NOTE_TYPES.SHARE)) failures++; - - // 9. DM Relay List - const dmRelayListInput = { - pubkey: TEST_PUBKEY, - created_at: TEST_TIMESTAMP, - relays: ['wss://dm.relay.com'] - }; - if (!runTest('buildDmRelayListEvent', buildDmRelayListEvent, dmRelayListInput, NOTE_TYPES.DM_RELAY_LIST)) failures++; - - // 10. Profile Metadata - const profileInput = { - pubkey: TEST_PUBKEY, - created_at: TEST_TIMESTAMP, - metadata: { name: 'Alice', about: 'Test user' } - }; - if (!runTest('buildProfileMetadataEvent', buildProfileMetadataEvent, profileInput, NOTE_TYPES.PROFILE_METADATA)) failures++; - - // 11. Mute List - const muteListInput = { - pubkey: TEST_PUBKEY, - created_at: TEST_TIMESTAMP, - pTags: [TEST_PUBKEY] - }; - if (!runTest('buildMuteListEvent', buildMuteListEvent, muteListInput, NOTE_TYPES.MUTE_LIST)) failures++; - - // 12. Deletion - const deletionInput = { - pubkey: TEST_PUBKEY, - created_at: TEST_TIMESTAMP, - eventIds: ['e'.repeat(64)], - reason: 'Mistake' - }; - if (!runTest('buildDeletionEvent', buildDeletionEvent, deletionInput, NOTE_TYPES.DELETION)) failures++; - - // 13. Legacy DM - const legacyDmInput = { - pubkey: TEST_PUBKEY, - created_at: TEST_TIMESTAMP, - recipientPubkey: TEST_PUBKEY, - ciphertext: 'encrypted-stuff' - }; - if (!runTest('buildLegacyDirectMessageEvent', buildLegacyDirectMessageEvent, legacyDmInput, NOTE_TYPES.LEGACY_DM)) failures++; - - // 14. DM Attachment - const dmAttachmentInput = { - pubkey: TEST_PUBKEY, - created_at: TEST_TIMESTAMP, - recipientPubkey: TEST_PUBKEY, - attachment: { url: 'https://example.com/file.jpg', type: 'image/jpeg' } - }; - if (!runTest('buildDmAttachmentEvent', buildDmAttachmentEvent, dmAttachmentInput, NOTE_TYPES.DM_ATTACHMENT)) failures++; - - // 15. DM Read Receipt - const dmReadReceiptInput = { - pubkey: TEST_PUBKEY, - created_at: TEST_TIMESTAMP, - recipientPubkey: TEST_PUBKEY, - eventId: 'e'.repeat(64) - }; - if (!runTest('buildDmReadReceiptEvent', buildDmReadReceiptEvent, dmReadReceiptInput, NOTE_TYPES.DM_READ_RECEIPT)) failures++; - - // 16. DM Typing Indicator - const dmTypingInput = { - pubkey: TEST_PUBKEY, - created_at: TEST_TIMESTAMP, - recipientPubkey: TEST_PUBKEY - }; - if (!runTest('buildDmTypingIndicatorEvent', buildDmTypingIndicatorEvent, dmTypingInput, NOTE_TYPES.DM_TYPING)) failures++; - - // 17. View Event - const viewInput = { - pubkey: TEST_PUBKEY, - created_at: TEST_TIMESTAMP, - pointerValue: 'test-pointer', - pointerTag: ['a', 'test-pointer'] - }; - if (!runTest('buildViewEvent', buildViewEvent, viewInput, NOTE_TYPES.VIEW_EVENT)) failures++; - - // 18. Zap Request - const zapRequestInput = { - pubkey: TEST_PUBKEY, - created_at: TEST_TIMESTAMP, - recipientPubkey: TEST_PUBKEY, - amountSats: 100, - relays: ['wss://relay.zap.com'] - }; - if (!runTest('buildZapRequestEvent', buildZapRequestEvent, zapRequestInput, NOTE_TYPES.ZAP_REQUEST)) failures++; - - // 19. Reaction - const reactionInput = { - pubkey: TEST_PUBKEY, - created_at: TEST_TIMESTAMP, - pointerTag: ['e', 'e'.repeat(64)], - content: '+' - }; - if (!runTest('buildReactionEvent', buildReactionEvent, reactionInput, NOTE_TYPES.VIDEO_REACTION)) failures++; - - // 20. Comment - const commentInput = { - pubkey: TEST_PUBKEY, - created_at: TEST_TIMESTAMP, - videoEventId: 'e'.repeat(64), - content: 'Nice video!' - }; - if (!runTest('buildCommentEvent', buildCommentEvent, commentInput, NOTE_TYPES.VIDEO_COMMENT)) failures++; - - // 21. Watch History - const watchHistoryInput = { - pubkey: TEST_PUBKEY, - created_at: TEST_TIMESTAMP, - monthIdentifier: '2023-01', - content: JSON.stringify({ ['e'.repeat(64)]: 123456 }) - }; - if (!runTest('buildWatchHistoryEvent', buildWatchHistoryEvent, watchHistoryInput, NOTE_TYPES.WATCH_HISTORY)) failures++; - - // 22. Subscription List - const subListInput = { - pubkey: TEST_PUBKEY, - created_at: TEST_TIMESTAMP - }; - if (!runTest('buildSubscriptionListEvent', buildSubscriptionListEvent, subListInput, NOTE_TYPES.SUBSCRIPTION_LIST)) failures++; - - // 23. Block List - const blockListInput = { - pubkey: TEST_PUBKEY, - created_at: TEST_TIMESTAMP - }; - if (!runTest('buildBlockListEvent', buildBlockListEvent, blockListInput, NOTE_TYPES.USER_BLOCK_LIST)) failures++; - - // 24. Admin List - const adminListInput = { - pubkey: TEST_PUBKEY, - created_at: TEST_TIMESTAMP, - hexPubkeys: [TEST_PUBKEY] - }; - if (!runTest('buildAdminListEvent (moderation)', (i) => buildAdminListEvent(ADMIN_LIST_IDENTIFIERS.moderation, i), adminListInput, NOTE_TYPES.ADMIN_MODERATION_LIST)) failures++; - - // 25. NIP-71 Video - const nip71Input = { - pubkey: TEST_PUBKEY, - title: 'NIP-71 Video', - metadata: { - kind: 21, - title: 'NIP-71 Video', - publishedAt: TEST_TIMESTAMP, - hashtags: ['test'] - }, - pointerIdentifiers: { videoRootId: 'test-root' } - }; - if (!runTest('buildNip71VideoEvent', buildNip71VideoEvent, nip71Input, NOTE_TYPES.NIP71_VIDEO)) failures++; - - // 26. NIP-17 Gift Wrap - const giftWrapInput = { - pubkey: TEST_PUBKEY, - created_at: TEST_TIMESTAMP, - recipientPubkey: TEST_PUBKEY, - ciphertext: 'encrypted-seal', - relayHint: 'wss://relay.example.com' - }; - if (!runTest('buildGiftWrapEvent', buildGiftWrapEvent, giftWrapInput, NOTE_TYPES.GIFT_WRAP)) failures++; - - // 27. NIP-17 Seal - const sealInput = { - pubkey: TEST_PUBKEY, - created_at: TEST_TIMESTAMP, - ciphertext: 'encrypted-rumor' - }; - if (!runTest('buildSealEvent', buildSealEvent, sealInput, NOTE_TYPES.SEAL)) failures++; - - // 28. NIP-17 Chat Message - const chatMessageInput = { - pubkey: TEST_PUBKEY, - created_at: TEST_TIMESTAMP, - recipientPubkey: TEST_PUBKEY, - content: 'Hello World' - }; - if (!runTest('buildChatMessageEvent', buildChatMessageEvent, chatMessageInput, NOTE_TYPES.CHAT_MESSAGE)) failures++; + // Compute summary + const allResults = [...report.builder_results, ...report.sanitizer_results, ...report.schema_registry_results]; + report.summary.total = allResults.length; + report.summary.passed = allResults.filter(r => r.validation.status === 'PASS').length; + report.summary.failed = allResults.filter(r => r.validation.status !== 'PASS').length; + // Write report + if (cliArgs.out && !cliArgs.dryRun) { + const outDir = path.dirname(cliArgs.out); + if (!fs.existsSync(outDir)) { + fs.mkdirSync(outDir, { recursive: true }); + } + fs.writeFileSync(cliArgs.out, JSON.stringify(report, null, 2)); + console.log(`\n📄 Report written to: ${cliArgs.out}`); + } console.log('\n-----------------------------------'); if (failures === 0) { - console.log('🎉 All schema validation tests passed!'); + console.log(`✓ All ${report.summary.total} validation checks passed!`); process.exit(0); } else { - console.error(`💥 ${failures} tests failed.`); + console.error(`${failures} checks failed out of ${report.summary.total}.`); process.exit(1); } } From f794971bfc31c17b030b40ccb7f4836090b576d6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Feb 2026 05:21:25 +0000 Subject: [PATCH 3/3] fix(agents): rewrite scheduler prompts to force pre-flight execution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agents were skipping the pre-flight curl/gh check for in-progress work, causing duplicate task runs. Root causes: 1. Two-document indirection (meta prompt → scheduler) caused the pre-flight instructions to get lost when the agent opened the second file 2. No proof-of-execution gate — agents could assert "no conflicts" without actually running the commands 3. curl|jq pipeline silently fails when jq is unavailable 4. "Step 0" labeling felt like skippable preamble Changes: - Make the commands the literal first instruction ("run NOW, paste output") - Use gh CLI as primary (authenticated, no jq needed), curl as fallback - Require raw terminal output pasted before proceeding (output gate) - Rename "Step 0" to "PRE-FLIGHT GATE" to signal it's not optional - Eliminate duplication between meta prompt and scheduler files - Add explicit "STOP HERE" checkpoint after command execution - Add skip-ahead clause for agents arriving from meta prompt https://claude.ai/code/session_01S7jaQb9ctJ4bzqRhEcm28Q --- docs/agents/prompts/META_PROMPTS.md | 134 +++++++++++++----------- docs/agents/prompts/daily-scheduler.md | 62 ++++++----- docs/agents/prompts/weekly-scheduler.md | 62 ++++++----- 3 files changed, 149 insertions(+), 109 deletions(-) diff --git a/docs/agents/prompts/META_PROMPTS.md b/docs/agents/prompts/META_PROMPTS.md index e95f75b4..5ac32554 100644 --- a/docs/agents/prompts/META_PROMPTS.md +++ b/docs/agents/prompts/META_PROMPTS.md @@ -1,6 +1,6 @@ # Bitvid Agent Scheduler Meta Prompts -This file contains the authoritative "Meta Prompts" to be used when triggering the daily and weekly agent schedulers. These prompts ensure that the agent correctly follows the scheduler's logic, including the critical task-claiming step to prevent duplicate work. +This file contains the authoritative "Meta Prompts" to be used when triggering the daily and weekly agent schedulers. Copy-paste the relevant prompt below into the agent session. --- @@ -9,42 +9,48 @@ This file contains the authoritative "Meta Prompts" to be used when triggering t ```text You are the bitvid daily agent scheduler. -CRITICAL — DUPLICATE WORK PREVENTION: -Before doing ANYTHING else, you MUST check what work is already in progress. -Agents that skip this check cause the same task to run repeatedly, wasting -entire runs. This has happened before. Do not let it happen again. +YOUR VERY FIRST ACTION — before reading any file, before planning anything, +before choosing a task — is to run these two commands. Not later. NOW. -Follow these steps IN ORDER. Do not skip or reorder any step. +COMMAND 1 — Check for open daily agent PRs: -STEP 0 — PRE-FLIGHT SCAN (do this FIRST, before choosing a task): - a) Run: curl -s "https://api.github.com/repos/PR0M3TH3AN/bitvid/pulls?state=open&per_page=100" | jq -c '.[] | select(.title | contains("[daily]")) | {number: .number, title: .title, created_at: .created_at, author: .user.login, url: .html_url}' - Record ALL open daily PRs. Every agent name in those titles is OFF LIMITS. - IMPORTANT: this query intentionally filters to titles containing "[daily]". It will NOT list unrelated open PRs. - (Optional visibility check) If you need total open PR context, run a second command without the filter. - b) Run: ls docs/agents/task-logs/daily/ | sort - Look for any "_started.md" files that do NOT have a corresponding - "_completed.md" or "_failed.md" file from the same agent at a later - timestamp. Those agents are OFF LIMITS (unless the started file is >24h old). - c) Write down your exclusion list before proceeding. - If the `curl` command fails, you MUST still check the task log directory. + gh pr list --repo PR0M3TH3AN/bitvid --state open --json number,title,createdAt,author --limit 100 | grep -i "\[daily\]" -STEP 1 — Read `AGENTS.md` and `CLAUDE.md` for project rules. + If `gh` is not available, use this fallback: + curl -s "https://api.github.com/repos/PR0M3TH3AN/bitvid/pulls?state=open&per_page=100" | grep -o '"title":"[^"]*\[daily\][^"]*"' -STEP 2 — Read `docs/agents/prompts/daily-scheduler.md` and follow its - instructions completely, starting from Step 1 (Determine the Next Task). - You already completed Step 0 above — use the exclusion list you built. - - When selecting the next agent, SKIP any agent on your exclusion list. - - Before executing: create a draft PR claim AND create a "started" log file - in docs/agents/task-logs/daily/. - - These two actions MUST happen before any task execution begins. +COMMAND 2 — Check for in-progress task logs: -STEP 3 — Once the task is claimed and executed: - - Create a new "completed" or "failed" log file in docs/agents/task-logs/daily/. - - Do NOT modify the "started" file — always create a new file. - - Commit and push your changes. + ls docs/agents/task-logs/daily/ | sort -REMEMBER: The #1 failure mode is agents not checking for in-progress work -and re-running the same task. Your pre-flight scan in Step 0 prevents this. +Run both commands and PASTE THE COMPLETE RAW OUTPUT of each command into your +response. Do not summarize. Do not paraphrase. Paste the actual terminal output. +If a command returns nothing, write: "OUTPUT: (empty — no results)" + +STOP HERE. Do not proceed until both outputs are pasted. + +--- + +Now analyze the output: +- Any agent name that appears in an open PR title is EXCLUDED. +- Any agent with a "_started.md" log file that has no matching "_completed.md" + or "_failed.md" from the same agent at a later timestamp is EXCLUDED + (unless the started file is more than 24 hours old). +- Write your exclusion list. Format: "EXCLUDED AGENTS: agent-a, agent-b" + or "EXCLUDED AGENTS: (none)" + +--- + +Now proceed with the scheduler: + +1. Read `AGENTS.md` and `CLAUDE.md` for project rules. +2. Read `docs/agents/prompts/daily-scheduler.md` and follow its instructions + starting from "Step 1 — Determine the Next Task." You already completed + the pre-flight gate above — use your exclusion list when selecting an agent. +3. Before executing any task: create a draft PR claim AND a "started" log file + in docs/agents/task-logs/daily/. Both MUST exist before task execution begins. +4. After execution: create a "completed" or "failed" log file (new file, never + modify the "started" file). Commit and push. ``` ## Weekly Scheduler Meta Prompt @@ -52,40 +58,46 @@ and re-running the same task. Your pre-flight scan in Step 0 prevents this. ```text You are the bitvid weekly agent scheduler. -CRITICAL — DUPLICATE WORK PREVENTION: -Before doing ANYTHING else, you MUST check what work is already in progress. -Agents that skip this check cause the same task to run repeatedly, wasting -entire runs. This has happened before. Do not let it happen again. +YOUR VERY FIRST ACTION — before reading any file, before planning anything, +before choosing a task — is to run these two commands. Not later. NOW. -Follow these steps IN ORDER. Do not skip or reorder any step. +COMMAND 1 — Check for open weekly agent PRs: -STEP 0 — PRE-FLIGHT SCAN (do this FIRST, before choosing a task): - a) Run: curl -s "https://api.github.com/repos/PR0M3TH3AN/bitvid/pulls?state=open&per_page=100" | jq -c '.[] | select(.title | contains("[weekly]")) | {number: .number, title: .title, created_at: .created_at, author: .user.login, url: .html_url}' - Record ALL open weekly PRs. Every agent name in those titles is OFF LIMITS. - IMPORTANT: this query intentionally filters to titles containing "[weekly]". It will NOT list unrelated open PRs. - (Optional visibility check) If you need total open PR context, run a second command without the filter. - b) Run: ls docs/agents/task-logs/weekly/ | sort - Look for any "_started.md" files that do NOT have a corresponding - "_completed.md" or "_failed.md" file from the same agent at a later - timestamp. Those agents are OFF LIMITS (unless the started file is >24h old). - c) Write down your exclusion list before proceeding. - If the `curl` command fails, you MUST still check the task log directory. + gh pr list --repo PR0M3TH3AN/bitvid --state open --json number,title,createdAt,author --limit 100 | grep -i "\[weekly\]" -STEP 1 — Read `AGENTS.md` and `CLAUDE.md` for project rules. + If `gh` is not available, use this fallback: + curl -s "https://api.github.com/repos/PR0M3TH3AN/bitvid/pulls?state=open&per_page=100" | grep -o '"title":"[^"]*\[weekly\][^"]*"' -STEP 2 — Read `docs/agents/prompts/weekly-scheduler.md` and follow its - instructions completely, starting from Step 1 (Determine the Next Task). - You already completed Step 0 above — use the exclusion list you built. - - When selecting the next agent, SKIP any agent on your exclusion list. - - Before executing: create a draft PR claim AND create a "started" log file - in docs/agents/task-logs/weekly/. - - These two actions MUST happen before any task execution begins. +COMMAND 2 — Check for in-progress task logs: -STEP 3 — Once the task is claimed and executed: - - Create a new "completed" or "failed" log file in docs/agents/task-logs/weekly/. - - Do NOT modify the "started" file — always create a new file. - - Commit and push your changes. + ls docs/agents/task-logs/weekly/ | sort -REMEMBER: The #1 failure mode is agents not checking for in-progress work -and re-running the same task. Your pre-flight scan in Step 0 prevents this. +Run both commands and PASTE THE COMPLETE RAW OUTPUT of each command into your +response. Do not summarize. Do not paraphrase. Paste the actual terminal output. +If a command returns nothing, write: "OUTPUT: (empty — no results)" + +STOP HERE. Do not proceed until both outputs are pasted. + +--- + +Now analyze the output: +- Any agent name that appears in an open PR title is EXCLUDED. +- Any agent with a "_started.md" log file that has no matching "_completed.md" + or "_failed.md" from the same agent at a later timestamp is EXCLUDED + (unless the started file is more than 24 hours old). +- Write your exclusion list. Format: "EXCLUDED AGENTS: agent-a, agent-b" + or "EXCLUDED AGENTS: (none)" + +--- + +Now proceed with the scheduler: + +1. Read `AGENTS.md` and `CLAUDE.md` for project rules. +2. Read `docs/agents/prompts/weekly-scheduler.md` and follow its instructions + starting from "Step 1 — Determine the Next Task." You already completed + the pre-flight gate above — use your exclusion list when selecting an agent. +3. Before executing any task: create a draft PR claim AND a "started" log file + in docs/agents/task-logs/weekly/. Both MUST exist before task execution begins. +4. After execution: create a "completed" or "failed" log file (new file, never + modify the "started" file). Commit and push. ``` diff --git a/docs/agents/prompts/daily-scheduler.md b/docs/agents/prompts/daily-scheduler.md index 267876ca..00117912 100644 --- a/docs/agents/prompts/daily-scheduler.md +++ b/docs/agents/prompts/daily-scheduler.md @@ -64,45 +64,59 @@ To find all entries for a specific agent: ls docs/agents/task-logs/daily/ | grep "" ``` -To check for in-progress (`started`) entries without a matching `completed`/`failed`: -```bash -ls docs/agents/task-logs/daily/ | grep "_started.md" -``` -Then verify each `started` file has a corresponding `completed` or `failed` file from the same agent at a later timestamp. If not, that agent is still in progress. - --- -## Step 0 — Pre-Flight: Scan ALL In-Flight Work (MANDATORY) +## PRE-FLIGHT GATE — Run These Commands NOW -> **DO NOT SKIP THIS STEP. This is the single most important step in the entire scheduler. If you skip this, you will cause duplicate work and waste an entire agent run.** +> **This gate is NOT optional. You MUST run both commands below and paste their raw output before doing anything else. If you skip this, you will duplicate another agent's work and waste the entire run. This has happened repeatedly.** -Before determining which agent to run, you MUST build situational awareness of what is already in progress. Run these commands and record the results: +If you arrived here from the meta prompt and already ran these commands and pasted the output, you may skip ahead to Step 1. Otherwise: -### 0a. List ALL open daily agent PRs +### COMMAND 1 — Check for open daily agent PRs +Run this command: ```bash -curl -s "https://api.github.com/repos/PR0M3TH3AN/bitvid/pulls?state=open&per_page=100" | jq -c '.[] | select(.title | contains("[daily]")) | {number: .number, title: .title, created_at: .created_at, author: .user.login}' +gh pr list --repo PR0M3TH3AN/bitvid --state open --json number,title,createdAt,author --limit 100 | grep -i "\[daily\]" ``` -Record every open PR. These represent **active claims by other agents**. Any agent whose name appears in these PR titles is OFF LIMITS. +If `gh` is unavailable, use this fallback: +```bash +curl -s "https://api.github.com/repos/PR0M3TH3AN/bitvid/pulls?state=open&per_page=100" | grep -o '"title":"[^"]*\[daily\][^"]*"' +``` -### 0b. Check the task log for incomplete runs +**Paste the complete raw output.** If the command returns nothing, write: `OUTPUT: (empty — no results)` +Every agent name that appears in these PR titles is **OFF LIMITS** — do not select it. + +### COMMAND 2 — Check the task log for incomplete runs + +Run this command: ```bash ls docs/agents/task-logs/daily/ | sort ``` -Look for any `_started.md` files that do NOT have a corresponding `_completed.md` or `_failed.md` file from the same agent at a later timestamp. These represent agents that began work but haven't finished. Treat them the same as open PRs: those agents are OFF LIMITS unless the `started` entry is **older than 24 hours** (stale/abandoned — check the date in the filename). +**Paste the complete raw output.** -### 0c. Build your exclusion list +Look for any `_started.md` files that do NOT have a corresponding `_completed.md` or `_failed.md` file from the same agent at a later timestamp. Those agents are **OFF LIMITS** (unless the started file is more than 24 hours old — check the date in the filename). -Combine the results from 0a and 0b into an explicit exclusion list: +### Build your exclusion list + +Combine results from Commands 1 and 2: - Agents with open/draft PRs → EXCLUDED - Agents with `started` status less than 24 hours old (no matching `completed`/`failed`) → EXCLUDED -Write this exclusion list down before proceeding. You will reference it in Step 1. +Write your exclusion list explicitly: +``` +EXCLUDED AGENTS: agent-a, agent-b +``` +or: +``` +EXCLUDED AGENTS: (none) +``` -**If the `curl` command fails** (e.g., network error, API limit): You MUST still check the task log directory for `started` entries. Log a warning in your summary that PR-based claim checking was unavailable. +**If Command 1 failed** (network error, API limit, `gh`/`curl` unavailable): you MUST still check the task log directory (Command 2). Log a warning in your summary that PR-based claim checking was unavailable. + +**Do not proceed to Step 1 until you have pasted command outputs and written your exclusion list.** --- @@ -114,7 +128,7 @@ Write this exclusion list down before proceeding. You will reference it in Step 4. Look up that agent in the alphabetical roster below and select the **next agent** in the list. 5. If the most recent agent is the **last** in the roster, wrap around to the **first**. 6. If the directory is empty, start with the **first** agent in the roster. -7. **CHECK YOUR EXCLUSION LIST FROM STEP 0.** If the selected agent is on the exclusion list, skip to the next agent in the roster. Repeat until you find an agent that is NOT excluded. If you cycle through the entire roster and ALL agents are excluded, log as `failed` with summary `"All roster tasks currently claimed by other agents"` and stop. +7. **CHECK YOUR EXCLUSION LIST.** If the selected agent is on the exclusion list, skip to the next agent in the roster. Repeat until you find an agent that is NOT excluded. If you cycle through the entire roster and ALL agents are excluded, log as `failed` with summary `"All roster tasks currently claimed by other agents"` and stop. ### Agent Roster (alphabetical order) @@ -166,13 +180,13 @@ You must create a visible claim before doing any work. This claim is a **distrib After creating the draft PR, immediately re-check: ```bash -curl -s "https://api.github.com/repos/PR0M3TH3AN/bitvid/pulls?state=open&per_page=100" | jq -c '.[] | select(.title | contains("[daily] ")) | {number: .number, title: .title, created_at: .created_at}' +gh pr list --repo PR0M3TH3AN/bitvid --state open --json number,title,createdAt --limit 100 | grep -i "" ``` -If you see another PR for this agent that was created *before* yours (by a different agent instance), close your PR with `gh pr close ` (if `gh` works) and go back to Step 1 to select the next agent. +If you see another PR for this agent that was created *before* yours (by a different agent instance), close your PR with `gh pr close ` and go back to Step 1 to select the next agent. ### 2c. Log "started" in the task log immediately -> **This is critical.** Create a `started` log file NOW, before executing the task. This ensures the rotation advances even if you crash during execution. +> **Create a `started` log file NOW, before executing the task. This ensures the rotation advances even if you crash during execution.** Create a new file in `docs/agents/task-logs/daily/` with this naming pattern: @@ -273,8 +287,8 @@ Use the current UTC timestamp (which will be later than the `started` file). The | Layer | Mechanism | Catches | |-------|-----------|---------| -| **Step 0** | Scan ALL open `[daily]` PRs | Agents claimed by any platform | -| **Step 0** | Check task log dir for `started` files | Agents in progress (even without PR) | +| **Pre-flight gate** | Scan ALL open `[daily]` PRs via `gh`/`curl` | Agents claimed by any platform | +| **Pre-flight gate** | Check task log dir for `started` files | Agents in progress (even without PR) | | **Step 2a** | Create draft PR as lock | Cross-platform visibility | | **Step 2b** | Race condition re-check | Simultaneous claims | | **Step 2c** | Create `started` log file immediately | Crash recovery — rotation advances | diff --git a/docs/agents/prompts/weekly-scheduler.md b/docs/agents/prompts/weekly-scheduler.md index 0a404912..af0349c1 100644 --- a/docs/agents/prompts/weekly-scheduler.md +++ b/docs/agents/prompts/weekly-scheduler.md @@ -64,45 +64,59 @@ To find all entries for a specific agent: ls docs/agents/task-logs/weekly/ | grep "" ``` -To check for in-progress (`started`) entries without a matching `completed`/`failed`: -```bash -ls docs/agents/task-logs/weekly/ | grep "_started.md" -``` -Then verify each `started` file has a corresponding `completed` or `failed` file from the same agent at a later timestamp. If not, that agent is still in progress. - --- -## Step 0 — Pre-Flight: Scan ALL In-Flight Work (MANDATORY) +## PRE-FLIGHT GATE — Run These Commands NOW -> **DO NOT SKIP THIS STEP. This is the single most important step in the entire scheduler. If you skip this, you will cause duplicate work and waste an entire agent run.** +> **This gate is NOT optional. You MUST run both commands below and paste their raw output before doing anything else. If you skip this, you will duplicate another agent's work and waste the entire run. This has happened repeatedly.** -Before determining which agent to run, you MUST build situational awareness of what is already in progress. Run these commands and record the results: +If you arrived here from the meta prompt and already ran these commands and pasted the output, you may skip ahead to Step 1. Otherwise: -### 0a. List ALL open weekly agent PRs +### COMMAND 1 — Check for open weekly agent PRs +Run this command: ```bash -curl -s "https://api.github.com/repos/PR0M3TH3AN/bitvid/pulls?state=open&per_page=100" | jq -c '.[] | select(.title | contains("[weekly]")) | {number: .number, title: .title, created_at: .created_at, author: .user.login}' +gh pr list --repo PR0M3TH3AN/bitvid --state open --json number,title,createdAt,author --limit 100 | grep -i "\[weekly\]" ``` -Record every open PR. These represent **active claims by other agents**. Any agent whose name appears in these PR titles is OFF LIMITS. +If `gh` is unavailable, use this fallback: +```bash +curl -s "https://api.github.com/repos/PR0M3TH3AN/bitvid/pulls?state=open&per_page=100" | grep -o '"title":"[^"]*\[weekly\][^"]*"' +``` -### 0b. Check the task log for incomplete runs +**Paste the complete raw output.** If the command returns nothing, write: `OUTPUT: (empty — no results)` +Every agent name that appears in these PR titles is **OFF LIMITS** — do not select it. + +### COMMAND 2 — Check the task log for incomplete runs + +Run this command: ```bash ls docs/agents/task-logs/weekly/ | sort ``` -Look for any `_started.md` files that do NOT have a corresponding `_completed.md` or `_failed.md` file from the same agent at a later timestamp. These represent agents that began work but haven't finished. Treat them the same as open PRs: those agents are OFF LIMITS unless the `started` entry is **older than 24 hours** (stale/abandoned — check the date in the filename). +**Paste the complete raw output.** -### 0c. Build your exclusion list +Look for any `_started.md` files that do NOT have a corresponding `_completed.md` or `_failed.md` file from the same agent at a later timestamp. Those agents are **OFF LIMITS** (unless the started file is more than 24 hours old — check the date in the filename). -Combine the results from 0a and 0b into an explicit exclusion list: +### Build your exclusion list + +Combine results from Commands 1 and 2: - Agents with open/draft PRs → EXCLUDED - Agents with `started` status less than 24 hours old (no matching `completed`/`failed`) → EXCLUDED -Write this exclusion list down before proceeding. You will reference it in Step 1. +Write your exclusion list explicitly: +``` +EXCLUDED AGENTS: agent-a, agent-b +``` +or: +``` +EXCLUDED AGENTS: (none) +``` -**If the `curl` command fails** (e.g., network error, API limit): You MUST still check the task log directory for `started` entries. Log a warning in your summary that PR-based claim checking was unavailable. +**If Command 1 failed** (network error, API limit, `gh`/`curl` unavailable): you MUST still check the task log directory (Command 2). Log a warning in your summary that PR-based claim checking was unavailable. + +**Do not proceed to Step 1 until you have pasted command outputs and written your exclusion list.** --- @@ -114,7 +128,7 @@ Write this exclusion list down before proceeding. You will reference it in Step 4. Look up that agent in the alphabetical roster below and select the **next agent** in the list. 5. If the most recent agent is the **last** in the roster, wrap around to the **first**. 6. If the directory is empty, start with the **first** agent in the roster. -7. **CHECK YOUR EXCLUSION LIST FROM STEP 0.** If the selected agent is on the exclusion list, skip to the next agent in the roster. Repeat until you find an agent that is NOT excluded. If you cycle through the entire roster and ALL agents are excluded, log as `failed` with summary `"All roster tasks currently claimed by other agents"` and stop. +7. **CHECK YOUR EXCLUSION LIST.** If the selected agent is on the exclusion list, skip to the next agent in the roster. Repeat until you find an agent that is NOT excluded. If you cycle through the entire roster and ALL agents are excluded, log as `failed` with summary `"All roster tasks currently claimed by other agents"` and stop. ### Agent Roster (alphabetical order) @@ -161,13 +175,13 @@ You must create a visible claim before doing any work. This claim is a **distrib After creating the draft PR, immediately re-check: ```bash -curl -s "https://api.github.com/repos/PR0M3TH3AN/bitvid/pulls?state=open&per_page=100" | jq -c '.[] | select(.title | contains("[weekly] ")) | {number: .number, title: .title, created_at: .created_at}' +gh pr list --repo PR0M3TH3AN/bitvid --state open --json number,title,createdAt --limit 100 | grep -i "" ``` -If you see another PR for this agent that was created *before* yours (by a different agent instance), close your PR with `gh pr close ` (if `gh` works) and go back to Step 1 to select the next agent. +If you see another PR for this agent that was created *before* yours (by a different agent instance), close your PR with `gh pr close ` and go back to Step 1 to select the next agent. ### 2c. Log "started" in the task log immediately -> **This is critical.** Create a `started` log file NOW, before executing the task. This ensures the rotation advances even if you crash during execution. +> **Create a `started` log file NOW, before executing the task. This ensures the rotation advances even if you crash during execution.** Create a new file in `docs/agents/task-logs/weekly/` with this naming pattern: @@ -270,8 +284,8 @@ Use the current UTC timestamp (which will be later than the `started` file). The | Layer | Mechanism | Catches | |-------|-----------|---------| -| **Step 0** | Scan ALL open `[weekly]` PRs | Agents claimed by any platform | -| **Step 0** | Check task log dir for `started` files | Agents in progress (even without PR) | +| **Pre-flight gate** | Scan ALL open `[weekly]` PRs via `gh`/`curl` | Agents claimed by any platform | +| **Pre-flight gate** | Check task log dir for `started` files | Agents in progress (even without PR) | | **Step 2a** | Create draft PR as lock | Cross-platform visibility | | **Step 2b** | Race condition re-check | Simultaneous claims | | **Step 2c** | Create `started` log file immediately | Crash recovery — rotation advances |