diff --git a/.agentworkforce/agents/relay-feature-guardian/agent.test.ts b/.agentworkforce/agents/relay-feature-guardian/agent.test.ts index 72252a491..edee94a1e 100644 --- a/.agentworkforce/agents/relay-feature-guardian/agent.test.ts +++ b/.agentworkforce/agents/relay-feature-guardian/agent.test.ts @@ -2,7 +2,6 @@ import { readFileSync } from 'node:fs'; import type { WorkforceCtx } from '@agentworkforce/runtime'; import { bindPreviewTransport, - slackClient, type RelayTransport, type RelayTransportRequest, type RelayTransportWriteRequest, @@ -12,13 +11,8 @@ import { describe, expect, it, vi } from 'vitest'; import guardian, { CYCLE_STATE_PATH, ProgressStateConflictError, - SLACK_WRITEBACK_POLL_MS, - SLACK_WRITEBACK_TIMEOUT_MS, createHttpProgressStore, - deliveredSlackTs, - featurePostIdempotencyKey, resolveManifestPath, - runGuardian, type ProgressState, } from './agent.ts'; @@ -34,7 +28,7 @@ const persona = JSON.parse(readFileSync(new URL('./persona.json', import.meta.ur relayfileMount?: { requiredReadPaths?: unknown; writeOnlyPaths?: unknown }; } >; - inputs: { SLACK_CHANNEL: { default: string } }; + inputs: Record; memory: { enabled: boolean; scopes: string[]; ttlDays: number }; }; @@ -103,16 +97,17 @@ function progressState(checkedCount: number, generation = 1): ProgressState { const checkedIds = orderedManifestFeatures.slice(0, checkedCount).map((feature) => feature.id); return { kind: 'relay-feature-guardian:progress', - version: 3, + version: 4, generation, checkedIds, cycleStartedAt: generation === 1 ? '2026-07-18T10:26:47.981Z' : '2026-07-18T11:26:47.981Z', totalFeatures: 122, ...(checkedCount > 0 ? { - lastPost: { + lastCheck: { featureId: checkedIds.at(-1) as string, - ts: `1784370${checkedCount}.029509`, + checkedAt: '2026-07-18T10:27:47.981Z', + evidence: 'log-only', }, } : {}), @@ -127,7 +122,7 @@ class RelayfileStateServer { corruptReadBack = false; readonly requests: Array<{ method: string; ifMatch: string | null }> = []; - constructor(seed: ProgressState | null) { + constructor(seed: object | null) { this.content = seed ? `${JSON.stringify(seed)}\n` : null; } @@ -205,34 +200,6 @@ class IdempotentSlackTransport implements RelayTransport { } } -class DelayedSlackTransport extends IdempotentSlackTransport { - constructor(private readonly delayMs: number) { - super(); - } - - override async write(request: RelayTransportWriteRequest): Promise { - await new Promise((resolve) => setTimeout(resolve, this.delayMs)); - const result = await super.write(request); - return { - ...result, - receipt: { externalId: ' ', ts: ` ${deliveredSlackTs(result)} ` }, - }; - } -} - -class LateReceiptReplaySlackTransport extends IdempotentSlackTransport { - private readonly receiptlessKeys = new Set(); - - override async write(request: RelayTransportWriteRequest): Promise { - const result = await super.write(request); - const body = request.body as { idempotencyKey?: string }; - const key = body.idempotencyKey ?? `unkeyed:${this.attempts.length}`; - if (this.receiptlessKeys.has(key)) return result; - this.receiptlessKeys.add(key); - return { ...result, receipt: { id: 'mountcmd-without-provider-ts' } }; - } -} - function guardianContext(failWriteCall: number): { ctx: WorkforceCtx; files: Map; @@ -322,118 +289,85 @@ function exactStateContext( } describe('relay-feature-guardian runtime paths', () => { - it('reads the manifest from the cloned relay repository', () => { - expect(resolveManifestPath('/home/daytona/workspace')).toBe( - '/home/daytona/workspace/github/repos/AgentWorkforce/relay/.agentworkforce/features/manifest.yaml' - ); - }); - - it('defaults delivery to the relay feature-check channel', () => { - expect(persona.inputs.SLACK_CHANNEL.default).toBe('C0AEKNLDNKW'); - }); - - it('falls back with every declared MCP surface when quiz generation fails', async () => { + it('records a healthy hourly check without writing a human Slack message', async () => { const transport = new IdempotentSlackTransport(); const restore = bindPreviewTransport(transport); - const mcpOnlyManifest = manifest.replace(' cli: relay node up', ' mcp: create_workspace'); - const { ctx } = exactStateContext(JSON.stringify(progressState(0)), mcpOnlyManifest); - ctx.llm.complete = vi.fn(async () => { - throw new Error('simulated quiz model failure'); - }); + const { ctx, files } = exactStateContext(JSON.stringify(progressState(0))); + (ctx.persona as { inputs: Record }).inputs = {}; try { await guardian.handler(ctx, { type: 'cron.tick' } as never); - const text = (transport.attempts[0]?.body as { text: string }).text; - expect(text).toContain('MCP tool: create_workspace'); - expect(text).not.toContain('CLI command:'); + + expect(transport.attempts).toHaveLength(0); + expect(ctx.llm.complete).toHaveBeenCalledWith(expect.stringContaining('CLI command: relay node up'), { + maxTokens: 300, + }); + expect(JSON.parse(files.get(CYCLE_STATE_PATH) ?? '{}')).toMatchObject({ + version: 4, + checkedIds: ['broker-up'], + lastCheck: { + featureId: 'broker-up', + evidence: 'log-only', + }, + }); + expect(ctx.log).toHaveBeenCalledWith( + 'info', + 'relay-feature-guardian.catalog-traversal-passed', + expect.objectContaining({ + feature: 'broker-up', + evidence: 'Is this feature working as expected?', + }) + ); } finally { restore(); } }); - it('uses a dedicated low-reasoning model path instead of shared subscription quota', () => { - expect(persona).toMatchObject({ - harness: 'opencode', - model: 'deepseek-v4-flash-free', - }); - expect(persona).not.toHaveProperty('useSubscription'); - }); - - it('declares bounded manifest and memory reads plus configured Slack output', () => { - expect(persona.integrations.github?.relayfileMount).toEqual({ - requiredReadPaths: ['/github/repos/AgentWorkforce/relay/.agentworkforce/features/**'], - writeOnlyPaths: [], - }); - expect(persona.memory).toEqual({ - enabled: true, - scopes: ['workspace'], - ttlDays: 14, - }); - expect(persona.integrations.slack).toMatchObject({ - optional: true, - enabledByInput: 'SLACK_CHANNEL', - relayfileMount: { - requiredReadPaths: [], - writeOnlyPaths: ['/slack/channels/${SLACK_CHANNEL}/**'], - }, - }); - }); - - it('deduplicates an ambiguous post retry and advances after a saved receipt', async () => { + it('uses deterministic log evidence when the optional model path fails', async () => { const transport = new IdempotentSlackTransport(); const restore = bindPreviewTransport(transport); - const { ctx, files } = guardianContext(2); + const seed = JSON.stringify(progressState(0)); + const { ctx, files } = exactStateContext(seed); + ctx.llm.complete = vi.fn(async () => { + throw new Error('simulated evidence failure'); + }); try { - // Run 1: the provider delivers Start Broker, then the exact progress - // checkpoint fails. Only the pre-post empty cycle remains. - await guardian.handler(ctx, { type: 'cron.tick' } as never); - expect(transport.providerCreates).toBe(1); - expect(JSON.parse(files.get(cycleStatePath) ?? '{}').checkedIds).toEqual([]); - - // Run 2: the same feature uses the same deterministic key, so the - // provider receipt is replayed rather than creating a duplicate post. - await guardian.handler(ctx, { type: 'cron.tick' } as never); - expect(transport.providerCreates).toBe(1); - expect(transport.attempts).toHaveLength(2); - const firstBody = transport.attempts[0].body as { idempotencyKey: string }; - const retryBody = transport.attempts[1].body as { idempotencyKey: string }; - expect(retryBody.idempotencyKey).toBe(firstBody.idempotencyKey); - - const completed = JSON.parse(files.get(cycleStatePath) ?? '{}') as { - checkedIds: string[]; - lastPost?: { featureId: string; ts: string }; - }; - expect(completed.checkedIds).toEqual(['broker-up']); - expect(completed.lastPost).toEqual({ - featureId: 'broker-up', - ts: '1710000001.000100', + await expect(guardian.handler(ctx, { type: 'cron.tick' } as never)).resolves.toBeUndefined(); + expect(transport.attempts).toHaveLength(0); + expect(JSON.parse(files.get(CYCLE_STATE_PATH) ?? '{}')).toMatchObject({ + checkedIds: ['broker-up'], + lastCheck: { featureId: 'broker-up', evidence: 'log-only' }, }); - - // Run 3: persisted progress selects the next feature in the cycle. - await guardian.handler(ctx, { type: 'cron.tick' } as never); - expect(transport.providerCreates).toBe(2); - const advanced = JSON.parse(files.get(cycleStatePath) ?? '{}') as { - checkedIds: string[]; - }; - expect(advanced.checkedIds).toEqual(['broker-up', 'broker-status']); + expect(ctx.log).toHaveBeenCalledWith( + 'warn', + 'relay-feature-guardian.evidence-fallback', + expect.objectContaining({ feature: 'broker-up', reason: 'Error: simulated evidence failure' }) + ); + expect(ctx.log).toHaveBeenCalledWith( + 'info', + 'relay-feature-guardian.catalog-traversal-passed', + expect.objectContaining({ evidence: expect.stringContaining('Catalog feature: Start Broker') }) + ); } finally { restore(); } }); - it('does not post when the initial cycle checkpoint has no receipt', async () => { + it('fails the run when the progress checkpoint fails and never posts success', async () => { const transport = new IdempotentSlackTransport(); const restore = bindPreviewTransport(transport); - const { ctx, files } = guardianContext(1); + const { ctx, files } = guardianContext(2); try { - await guardian.handler(ctx, { type: 'cron.tick' } as never); - expect(transport.providerCreates).toBe(0); - expect(files.size).toBe(0); + await expect(guardian.handler(ctx, { type: 'cron.tick' } as never)).rejects.toThrow( + 'simulated exact-state write failure' + ); + expect(transport.attempts).toHaveLength(0); + expect(JSON.parse(files.get(CYCLE_STATE_PATH) ?? '{}').checkedIds).toEqual([]); expect(ctx.log).toHaveBeenCalledWith( 'error', - 'relay-feature-guardian.cycle-checkpoint-failed', + 'relay-feature-guardian.progress-checkpoint-failed', expect.objectContaining({ err: expect.stringContaining('simulated exact-state write failure') }) ); } finally { @@ -441,7 +375,21 @@ describe('relay-feature-guardian runtime paths', () => { } }); - it('loads exact cycle state and advances 2/122 then 3/122 across fresh runs', async () => { + it('fails the run when the initial cycle checkpoint fails', async () => { + const { ctx, files } = guardianContext(1); + + await expect(guardian.handler(ctx, { type: 'cron.tick' } as never)).rejects.toThrow( + 'simulated exact-state write failure' + ); + expect(files.size).toBe(0); + expect(ctx.log).toHaveBeenCalledWith( + 'error', + 'relay-feature-guardian.cycle-checkpoint-failed', + expect.objectContaining({ err: expect.stringContaining('simulated exact-state write failure') }) + ); + }); + + it('migrates legacy Slack receipt state and advances silently across fresh runs', async () => { const transport = new IdempotentSlackTransport(); const restore = bindPreviewTransport(transport); const { ctx, files } = exactStateContext( @@ -458,25 +406,17 @@ describe('relay-feature-guardian runtime paths', () => { try { await guardian.handler(ctx, { type: 'cron.tick' } as never); - expect(transport.attempts).toHaveLength(1); - expect((transport.attempts[0].body as { text: string }).text).toContain( - 'relay feature check · 2/122 · 120 remaining in cycle' - ); - expect(JSON.parse(files.get(cycleStatePath) ?? '{}').checkedIds).toEqual([ - 'broker-up', - 'broker-status', - ]); - await guardian.handler(ctx, { type: 'cron.tick' } as never); - expect(transport.attempts).toHaveLength(2); - expect((transport.attempts[1].body as { text: string }).text).toContain( - 'relay feature check · 3/122 · 119 remaining in cycle' - ); - expect(JSON.parse(files.get(cycleStatePath) ?? '{}').checkedIds).toEqual([ - 'broker-up', - 'broker-status', - 'broker-down', - ]); + + expect(transport.attempts).toHaveLength(0); + expect(JSON.parse(files.get(CYCLE_STATE_PATH) ?? '{}')).toMatchObject({ + version: 4, + checkedIds: ['broker-up', 'broker-status', 'broker-down'], + lastCheck: { + featureId: 'broker-down', + evidence: 'log-only', + }, + }); expect(ctx.memory.recall).not.toHaveBeenCalled(); expect(ctx.memory.save).not.toHaveBeenCalled(); } finally { @@ -485,50 +425,38 @@ describe('relay-feature-guardian runtime paths', () => { }); it('preserves progress when the manifest adds features', async () => { - const transport = new IdempotentSlackTransport(); - const restore = bindPreviewTransport(transport); const staleTotal = { ...progressState(1), totalFeatures: 121 }; const { ctx, files } = exactStateContext(JSON.stringify(staleTotal)); - try { - await guardian.handler(ctx, { type: 'cron.tick' } as never); - const state = JSON.parse(files.get(CYCLE_STATE_PATH) ?? '{}') as ProgressState; - expect(state.totalFeatures).toBe(122); - expect(state.checkedIds).toEqual(['broker-up', 'broker-status']); - expect(state.lastPost?.featureId).toBe('broker-status'); - } finally { - restore(); - } + + await guardian.handler(ctx, { type: 'cron.tick' } as never); + + const state = JSON.parse(files.get(CYCLE_STATE_PATH) ?? '{}') as ProgressState; + expect(state.totalFeatures).toBe(122); + expect(state.checkedIds).toEqual(['broker-up', 'broker-status']); + expect(state.lastCheck).toMatchObject({ + featureId: 'broker-status', + evidence: 'log-only', + }); }); it('reconciles one unchecked feature retirement without resetting the generation', async () => { - const transport = new IdempotentSlackTransport(); - const restore = bindPreviewTransport(transport); const staleTotal = { ...progressState(1), totalFeatures: 123 }; const { ctx, files } = exactStateContext(JSON.stringify(staleTotal)); - try { - await guardian.handler(ctx, { type: 'cron.tick' } as never); - expect(transport.attempts).toHaveLength(1); - expect((transport.attempts[0].body as { text: string }).text).toContain( - 'relay feature check · 2/122 · 120 remaining in cycle' - ); - const state = JSON.parse(files.get(CYCLE_STATE_PATH) ?? '{}') as ProgressState; - expect(state.generation).toBe(1); - expect(state.totalFeatures).toBe(122); - expect(state.checkedIds).toEqual(['broker-up', 'broker-status']); - expect(state.lastPost).toEqual({ - featureId: 'broker-status', - ts: '1710000001.000100', - }); - expect(ctx.files.write).toHaveBeenCalledTimes(2); - } finally { - restore(); - } + await guardian.handler(ctx, { type: 'cron.tick' } as never); + + const state = JSON.parse(files.get(CYCLE_STATE_PATH) ?? '{}') as ProgressState; + expect(state.generation).toBe(1); + expect(state.totalFeatures).toBe(122); + expect(state.checkedIds).toEqual(['broker-up', 'broker-status']); + expect(state.lastCheck).toMatchObject({ + featureId: 'broker-status', + evidence: 'log-only', + }); + expect(ctx.files.write).toHaveBeenCalledTimes(2); }); it('resets a generation under CAS when the manifest retires a checked feature', async () => { - const transport = new IdempotentSlackTransport(); - const restore = bindPreviewTransport(transport); const { ctx, files } = exactStateContext( JSON.stringify({ kind: 'relay-feature-guardian:progress', @@ -541,26 +469,19 @@ describe('relay-feature-guardian runtime paths', () => { }) ); - try { - await guardian.handler(ctx, { type: 'cron.tick' } as never); - expect(transport.attempts).toHaveLength(1); - expect((transport.attempts[0].body as { text: string }).text).toContain( - 'relay feature check · 1/122 · 121 remaining in cycle' - ); - const state = JSON.parse(files.get(CYCLE_STATE_PATH) ?? '{}') as ProgressState; - expect(state.generation).toBe(8); - expect(state.totalFeatures).toBe(122); - expect(state.checkedIds).toEqual(['broker-up']); - expect(state.lastPost).toEqual({ - featureId: 'broker-up', - ts: '1710000001.000100', - }); - } finally { - restore(); - } + await guardian.handler(ctx, { type: 'cron.tick' } as never); + + const state = JSON.parse(files.get(CYCLE_STATE_PATH) ?? '{}') as ProgressState; + expect(state.generation).toBe(8); + expect(state.totalFeatures).toBe(122); + expect(state.checkedIds).toEqual(['broker-up']); + expect(state.lastCheck).toMatchObject({ + featureId: 'broker-up', + evidence: 'log-only', + }); }); - it('preserves exact progress when a suspiciously partial manifest omits checked features', async () => { + it('fails closed and preserves exact progress for a suspicious partial manifest', async () => { const transport = new IdempotentSlackTransport(); const restore = bindPreviewTransport(transport); const seed = JSON.stringify({ @@ -575,137 +496,91 @@ describe('relay-feature-guardian runtime paths', () => { const { ctx, files } = exactStateContext(seed, renderManifest(manifestFeatures.slice(0, 2))); try { - await guardian.handler(ctx, { type: 'cron.tick' } as never); - expect(transport.attempts).toHaveLength(0); - expect(JSON.parse(files.get(CYCLE_STATE_PATH) ?? '{}')).toEqual(JSON.parse(seed)); - expect(ctx.log).toHaveBeenCalledWith( - 'error', - 'relay-feature-guardian.progress-reconcile-failed', - expect.objectContaining({ - err: expect.stringContaining('refusing a partial-manifest reset'), - previousTotal: 123, - currentTotal: 2, - }) + await expect(guardian.handler(ctx, { type: 'cron.tick' } as never)).rejects.toThrow( + 'refusing a partial-manifest reset' ); - } finally { - restore(); - } - }); - - it('preserves exact progress when multiple checked feature IDs disappear at the same total', async () => { - const transport = new IdempotentSlackTransport(); - const restore = bindPreviewTransport(transport); - const seed = JSON.stringify({ - kind: 'relay-feature-guardian:progress', - version: 3, - generation: 7, - checkedIds: ['broker-up', 'retired-feature-a', 'retired-feature-b'], - cycleStartedAt: '2026-07-18T10:26:47.981Z', - totalFeatures: 122, - lastPost: { featureId: 'retired-feature-b', ts: '1784370419.029509' }, - }); - const { ctx, files } = exactStateContext(seed); - - try { - await guardian.handler(ctx, { type: 'cron.tick' } as never); expect(transport.attempts).toHaveLength(0); expect(JSON.parse(files.get(CYCLE_STATE_PATH) ?? '{}')).toEqual(JSON.parse(seed)); expect(ctx.log).toHaveBeenCalledWith( 'error', 'relay-feature-guardian.progress-reconcile-failed', - expect.objectContaining({ - err: expect.stringContaining('multiple checked feature ids disappeared'), - previousTotal: 122, - currentTotal: 122, - retiredIds: ['retired-feature-a', 'retired-feature-b'], - }) + expect.objectContaining({ previousTotal: 123, currentTotal: 2 }) ); } finally { restore(); } }); - it('preserves exact progress when the manifest read fails', async () => { - const transport = new IdempotentSlackTransport(); - const restore = bindPreviewTransport(transport); + it('fails closed and preserves progress when the manifest read fails', async () => { const seed = JSON.stringify(progressState(2)); const { ctx, files } = exactStateContext(seed); ctx.sandbox.readFile = vi.fn(async () => { throw new Error('simulated manifest read failure'); }); - try { - await guardian.handler(ctx, { type: 'cron.tick' } as never); - expect(JSON.parse(files.get(CYCLE_STATE_PATH) ?? '{}')).toEqual(JSON.parse(seed)); - expect(ctx.log).toHaveBeenCalledWith( - 'error', - 'relay-feature-guardian.manifest-load-failed', - expect.objectContaining({ err: expect.stringContaining('simulated manifest read failure') }) - ); - } finally { - restore(); - } + await expect(guardian.handler(ctx, { type: 'cron.tick' } as never)).rejects.toThrow( + 'simulated manifest read failure' + ); + expect(JSON.parse(files.get(CYCLE_STATE_PATH) ?? '{}')).toEqual(JSON.parse(seed)); + expect(ctx.log).toHaveBeenCalledWith( + 'error', + 'relay-feature-guardian.manifest-load-failed', + expect.objectContaining({ err: expect.stringContaining('simulated manifest read failure') }) + ); + }); + + it('fails the run when the parsed feature catalog is empty', async () => { + const { ctx, files } = exactStateContext(JSON.stringify(progressState(0)), renderManifest([])); + + await expect(guardian.handler(ctx, { type: 'cron.tick' } as never)).rejects.toThrow( + 'empty feature manifest' + ); + expect(files.get(CYCLE_STATE_PATH)).toBe(JSON.stringify(progressState(0))); + expect(ctx.log).toHaveBeenCalledWith( + 'error', + 'relay-feature-guardian.no-features', + expect.objectContaining({ reason: 'manifest parsed but empty' }) + ); }); it('fails closed outside invoke simulation when Relayfile credentials are absent', async () => { - const transport = new IdempotentSlackTransport(); - const restore = bindPreviewTransport(transport); const { ctx } = exactStateContext(JSON.stringify(progressState(1))); (ctx.agent as { id: string }).id = 'deployed-agent'; (ctx.deployment as { id: string }).id = 'deployed-run'; - try { - await guardian.handler(ctx, { type: 'cron.tick' } as never); - expect(transport.attempts).toHaveLength(0); - expect(ctx.log).toHaveBeenCalledWith( - 'error', - 'relay-feature-guardian.progress-load-failed', - expect.objectContaining({ err: expect.stringContaining('exact Relayfile credentials') }) - ); - } finally { - restore(); - } - }); - it('scopes provider idempotency to a feature within one cycle', () => { - expect(featurePostIdempotencyKey('cycle-a', 'start-broker')).toBe( - featurePostIdempotencyKey('cycle-a', 'start-broker') + await expect(guardian.handler(ctx, { type: 'cron.tick' } as never)).rejects.toThrow( + 'exact Relayfile credentials' ); - expect(featurePostIdempotencyKey('cycle-a', 'start-broker')).not.toBe( - featurePostIdempotencyKey('cycle-b', 'start-broker') + expect(ctx.log).toHaveBeenCalledWith( + 'error', + 'relay-feature-guardian.progress-load-failed', + expect.objectContaining({ err: expect.stringContaining('exact Relayfile credentials') }) ); }); - it('requires a delivered Slack ts instead of a draft receipt id', () => { - expect(deliveredSlackTs(undefined)).toBe(''); - expect(deliveredSlackTs(null)).toBe(''); - expect( - deliveredSlackTs({ - path: '/draft.json', - absolutePath: '/draft.json', - receipt: { id: 'mountcmd-draft', created: 'mountcmd-draft' }, - }) - ).toBe(''); - expect( - deliveredSlackTs({ - path: '/delivered.json', - absolutePath: '/delivered.json', - receipt: { externalId: '1710000001.000100' }, - }) - ).toBe('1710000001.000100'); - expect( - deliveredSlackTs({ - path: '/delivered-via-ts.json', - absolutePath: '/delivered-via-ts.json', - receipt: { externalId: ' ', ts: '1710000002.000200' }, - }) - ).toBe('1710000002.000200'); - expect( - deliveredSlackTs({ - path: '/invalid-external-id.json', - absolutePath: '/invalid-external-id.json', - receipt: { externalId: 'mountcmd-not-a-ts', ts: '1710000003.000300' }, - }) - ).toBe('1710000003.000300'); + it('reads the manifest from the cloned relay repository', () => { + expect(resolveManifestPath('/home/daytona/workspace')).toBe( + '/home/daytona/workspace/github/repos/AgentWorkforce/relay/.agentworkforce/features/manifest.yaml' + ); + }); + + it('uses a dedicated low-reasoning model path without a human output surface', () => { + expect(persona).toMatchObject({ + harness: 'opencode', + model: 'deepseek-v4-flash-free', + inputs: {}, + }); + expect(persona).not.toHaveProperty('useSubscription'); + expect(persona.integrations).not.toHaveProperty('slack'); + expect(persona.integrations.github?.relayfileMount).toEqual({ + requiredReadPaths: ['/github/repos/AgentWorkforce/relay/.agentworkforce/features/**'], + writeOnlyPaths: [], + }); + expect(persona.memory).toEqual({ + enabled: true, + scopes: ['workspace'], + ttlDays: 14, + }); }); }); @@ -746,13 +621,13 @@ describe('relay-feature-guardian exact HTTP state', () => { ProgressStateConflictError ); - expect(position2.state.lastPost?.featureId).toBe('broker-status'); - expect(position3.state.lastPost?.featureId).toBe('broker-down'); + expect(position2.state.lastCheck?.featureId).toBe('broker-status'); + expect(position3.state.lastCheck?.featureId).toBe('broker-down'); expect(server.state()).toEqual(progressState(3)); }); it('uses the loaded revision when resetting a genuinely retired feature', async () => { - const retiredState: ProgressState = { + const retiredState = { kind: 'relay-feature-guardian:progress', version: 3, generation: 7, @@ -768,7 +643,7 @@ describe('relay-feature-guardian exact HTTP state', () => { const reset: ProgressState = { kind: 'relay-feature-guardian:progress', - version: 3, + version: 4, generation: 8, checkedIds: [], cycleStartedAt: '2026-07-18T11:26:47.981Z', @@ -874,16 +749,17 @@ describe('relay-feature-guardian exact HTTP state', () => { })); const state = (checkedIds: string[]): ProgressState => ({ kind: 'relay-feature-guardian:progress', - version: 3, + version: 4, generation: 1, checkedIds, cycleStartedAt: '2026-07-18T10:26:47.981Z', totalFeatures: features.length, ...(checkedIds.length > 0 ? { - lastPost: { + lastCheck: { featureId: checkedIds.at(-1) as string, - ts: '1784370419.029509', + checkedAt: '2026-07-18T10:27:47.981Z', + evidence: 'log-only' as const, }, } : {}), @@ -903,130 +779,27 @@ describe('relay-feature-guardian exact HTTP state', () => { it.each([ ['duplicate ids', { ...progressState(2), checkedIds: ['broker-up', 'broker-up'] }], ['malformed historical id', { ...progressState(1), checkedIds: [''] }], - ['invalid ts', { ...progressState(1), lastPost: { featureId: 'broker-up', ts: 'not-a-slack-ts' } }], + [ + 'invalid check time', + { + ...progressState(1), + lastCheck: { featureId: 'broker-up', checkedAt: 'not-a-time', evidence: 'log-only' }, + }, + ], + [ + 'out-of-range legacy Slack timestamp', + { + ...progressState(1), + version: 3, + lastCheck: undefined, + lastPost: { featureId: 'broker-up', ts: '9007199254740991.0' }, + }, + ], ['invalid cycle time', { ...progressState(1), cycleStartedAt: 'yesterday' }], - ])('rejects bounded v3 state with %s', async (_label, invalid) => { + ])('rejects bounded state with %s', async (_label, invalid) => { const server = new RelayfileStateServer(null); server.content = `${JSON.stringify(invalid)}\n`; const store = createHttpProgressStore(credentials, { fetchImpl: server.fetch }); await expect(store.load(storeFeatures)).rejects.toThrow(/cycle state/); }); }); - -describe('relay-feature-guardian delayed Slack receipts', () => { - it('rejects the run when the Slack post fails so the runner records handler.error', async () => { - const failure = new Error('simulated Slack writeback failure'); - const { ctx, files } = exactStateContext(JSON.stringify(progressState(1))); - const createSlackClient = (() => ({ - messages: { - write: vi.fn(async () => { - throw failure; - }), - }, - })) as unknown as typeof slackClient; - - await expect(runGuardian(ctx, { type: 'cron.tick' } as never, { createSlackClient })).rejects.toBe( - failure - ); - - expect(JSON.parse(files.get(CYCLE_STATE_PATH) ?? '{}').checkedIds).toEqual(['broker-up']); - expect(ctx.log).toHaveBeenCalledWith('error', 'relay-feature-guardian.post-failed', { - channel: 'C0AEKNLDNKW', - feature: 'broker-status', - err: String(failure), - }); - }); - - it('passes the production receipt deadline and poll interval to the Slack helper', async () => { - const transport = new IdempotentSlackTransport(); - const restore = bindPreviewTransport(transport); - const { ctx } = exactStateContext(JSON.stringify(progressState(1))); - const observedOptions: unknown[] = []; - const createSlackClient = ((options?: Parameters[0]) => { - observedOptions.push(options); - return slackClient(options); - }) as typeof slackClient; - - try { - await runGuardian(ctx, { type: 'cron.tick' } as never, { createSlackClient }); - expect(observedOptions).toContainEqual({ - writebackTimeoutMs: 15_000, - writebackPollMs: 250, - }); - } finally { - restore(); - } - }); - - it('waits beyond the old 3s window and checkpoints only a real trimmed ts', async () => { - expect(SLACK_WRITEBACK_TIMEOUT_MS).toBe(15_000); - expect(SLACK_WRITEBACK_POLL_MS).toBe(250); - vi.useFakeTimers(); - const transport = new DelayedSlackTransport(3_500); - const restore = bindPreviewTransport(transport); - const { ctx, files } = exactStateContext(JSON.stringify(progressState(1))); - try { - const run = guardian.handler(ctx, { type: 'cron.tick' } as never); - await vi.advanceTimersByTimeAsync(3_001); - expect(JSON.parse(files.get(CYCLE_STATE_PATH) ?? '{}').checkedIds).toEqual(['broker-up']); - await vi.advanceTimersByTimeAsync(500); - await run; - - const state = JSON.parse(files.get(CYCLE_STATE_PATH) ?? '{}') as ProgressState; - expect(state.checkedIds).toEqual(['broker-up', 'broker-status']); - expect(state.lastPost?.ts).toBe('1710000001.000100'); - } finally { - restore(); - vi.useRealTimers(); - } - }); - - it('keeps a delayed receipt retryable and replays the stable key without a second Slack post', async () => { - const capabilityFeatures = [ - manifestFeatures[0]!, - { - id: 'capabilities-register', - name: 'Register Capabilities', - cli: 'relay capabilities register', - description: 'Registers broker capabilities.', - tier: 1, - }, - ]; - const capabilityState: ProgressState = { - kind: 'relay-feature-guardian:progress', - version: 3, - generation: 1, - checkedIds: ['broker-up'], - cycleStartedAt: '2026-07-18T10:26:47.981Z', - totalFeatures: capabilityFeatures.length, - lastPost: { featureId: 'broker-up', ts: '17843701.029509' }, - }; - const formerFatalError = 'Slack post failed: no timestamp returned for feature capabilities-register'; - const transport = new LateReceiptReplaySlackTransport(); - const restore = bindPreviewTransport(transport); - const { ctx, files } = exactStateContext( - JSON.stringify(capabilityState), - renderManifest(capabilityFeatures) - ); - try { - await expect(guardian.handler(ctx, { type: 'cron.tick' } as never)).resolves.toBeUndefined(); - expect(JSON.parse(files.get(CYCLE_STATE_PATH) ?? '{}').checkedIds).toEqual(['broker-up']); - expect(ctx.log).toHaveBeenCalledWith('warn', 'relay-feature-guardian.post-receipt-pending', { - channel: 'C0AEKNLDNKW', - feature: 'capabilities-register', - path: expect.any(String), - }); - expect(JSON.stringify(vi.mocked(ctx.log).mock.calls)).not.toContain(formerFatalError); - - await expect(guardian.handler(ctx, { type: 'cron.tick' } as never)).resolves.toBeUndefined(); - const state = JSON.parse(files.get(CYCLE_STATE_PATH) ?? '{}') as ProgressState; - expect(state.checkedIds).toEqual(['broker-up', 'capabilities-register']); - expect(state.lastPost?.ts).toBe('1710000001.000100'); - expect(transport.providerCreates).toBe(1); - expect(transport.attempts).toHaveLength(2); - expect(transport.attempts[0]?.body).toMatchObject(transport.attempts[1]?.body as object); - } finally { - restore(); - } - }); -}); diff --git a/.agentworkforce/agents/relay-feature-guardian/agent.ts b/.agentworkforce/agents/relay-feature-guardian/agent.ts index 7538e9388..3b81befa4 100644 --- a/.agentworkforce/agents/relay-feature-guardian/agent.ts +++ b/.agentworkforce/agents/relay-feature-guardian/agent.ts @@ -5,9 +5,9 @@ * 1. Read the feature list from the cloned relay repository * 2. Load feature progress from an exact, revisioned Relayfile record * 3. Pick the next unchecked feature (ordered by criticality then tier) - * 4. Generate a concise quiz question via ctx.llm - * 5. Post to Slack with @mentions for Will and Khaliq - * 6. Persist updated progress + * 4. Generate concise verification evidence via ctx.llm + * 5. Persist updated progress + * 6. Record success as structured log-only evidence * * After the full manifest is covered, the cycle resets. */ @@ -17,8 +17,6 @@ import { type WorkforceCtx, type WorkforceEvent, } from '@agentworkforce/runtime'; -import { input } from '@agentworkforce/delivery'; -import { slackClient, type WritebackResult } from '@relayfile/relay-helpers'; import { randomUUID } from 'node:crypto'; import { parse } from 'yaml'; @@ -101,21 +99,20 @@ async function loadFeatures(ctx: WorkforceCtx): Promise { export interface ProgressState { kind: 'relay-feature-guardian:progress'; - version: 3; + version: 4; generation: number; checkedIds: string[]; cycleStartedAt: string; totalFeatures: number; - lastPost?: { + lastCheck?: { featureId: string; - ts: string; + checkedAt: string; + evidence: 'legacy-slack' | 'log-only'; }; } export const CYCLE_STATE_PATH = '/memory/workspace/relay-feature-guardian/cycle-state.json'; export const STATE_IO_TIMEOUT_MS = 5_000; -export const SLACK_WRITEBACK_TIMEOUT_MS = 15_000; -export const SLACK_WRITEBACK_POLL_MS = 250; const MAX_STATE_BYTES = 64 * 1024; const MAX_SAFE_MANIFEST_SHRINK = 1; @@ -158,6 +155,14 @@ function isSlackTs(value: unknown): value is string { return typeof value === 'string' && /^\d+\.\d+$/.test(value.trim()); } +function slackTsToIso(value: string): string { + const seconds = Number(value.trim().split('.')[0]); + if (!Number.isSafeInteger(seconds)) throw new Error('cycle state lastPost is invalid'); + const timestamp = new Date(seconds * 1_000); + if (!Number.isFinite(timestamp.valueOf())) throw new Error('cycle state lastPost is invalid'); + return timestamp.toISOString(); +} + function assertStateSize(content: string): void { if (UTF8_ENCODER.encode(content).byteLength > MAX_STATE_BYTES) { throw new Error('cycle state exceeds size limit'); @@ -170,7 +175,7 @@ function parseProgressState( options: { allowHistoricalIds?: boolean } = {} ): ProgressState { if (!isRecord(value)) throw new Error('cycle state must be an object'); - if (value.kind !== 'relay-feature-guardian:progress' || value.version !== 3) { + if (value.kind !== 'relay-feature-guardian:progress' || (value.version !== 3 && value.version !== 4)) { throw new Error('cycle state kind/version is invalid'); } if (!Number.isSafeInteger(value.generation) || (value.generation as number) < 1) { @@ -206,8 +211,8 @@ function parseProgressState( throw new Error('cycle state contains duplicate feature ids'); } - let lastPost: ProgressState['lastPost']; - if (value.lastPost !== undefined) { + let lastCheck: ProgressState['lastCheck']; + if (value.version === 3 && value.lastPost !== undefined) { if ( !isRecord(value.lastPost) || typeof value.lastPost.featureId !== 'string' || @@ -216,26 +221,42 @@ function parseProgressState( ) { throw new Error('cycle state lastPost is invalid'); } - lastPost = { + lastCheck = { featureId: value.lastPost.featureId, - ts: value.lastPost.ts.trim(), + checkedAt: slackTsToIso(value.lastPost.ts), + evidence: 'legacy-slack', + }; + } else if (value.version === 4 && value.lastCheck !== undefined) { + if ( + !isRecord(value.lastCheck) || + typeof value.lastCheck.featureId !== 'string' || + !checkedIds.includes(value.lastCheck.featureId) || + !isCanonicalIsoTimestamp(value.lastCheck.checkedAt) || + (value.lastCheck.evidence !== 'legacy-slack' && value.lastCheck.evidence !== 'log-only') + ) { + throw new Error('cycle state lastCheck is invalid'); + } + lastCheck = { + featureId: value.lastCheck.featureId, + checkedAt: value.lastCheck.checkedAt, + evidence: value.lastCheck.evidence, }; } - if (checkedIds.length > 0 && !lastPost) { - throw new Error('cycle state with progress requires lastPost'); + if (checkedIds.length > 0 && !lastCheck) { + throw new Error('cycle state with progress requires lastCheck'); } - if (lastPost && lastPost.featureId !== checkedIds.at(-1)) { - throw new Error('cycle state lastPost must describe the latest checked feature'); + if (lastCheck && lastCheck.featureId !== checkedIds.at(-1)) { + throw new Error('cycle state lastCheck must describe the latest checked feature'); } return { kind: 'relay-feature-guardian:progress', - version: 3, + version: 4, generation: value.generation as number, checkedIds, cycleStartedAt: value.cycleStartedAt, totalFeatures: value.totalFeatures as number, - ...(lastPost ? { lastPost } : {}), + ...(lastCheck ? { lastCheck } : {}), }; } @@ -290,7 +311,7 @@ function assertValidTransition( features: Feature[] ): void { if (!previous) { - if (next.generation !== 1 || next.checkedIds.length !== 0 || next.lastPost) { + if (next.generation !== 1 || next.checkedIds.length !== 0 || next.lastCheck) { throw new Error('new cycle state must bootstrap an empty generation 1'); } return; @@ -309,10 +330,10 @@ function assertValidTransition( throw new Error('cycle progress cannot regress or skip a checkpoint'); } if (next.checkedIds.length === prior.checkedIds.length) { - if (JSON.stringify(next.lastPost) !== JSON.stringify(prior.lastPost)) { - throw new Error('reconciliation cannot overwrite lastPost'); + if (JSON.stringify(next.lastCheck) !== JSON.stringify(prior.lastCheck)) { + throw new Error('reconciliation cannot overwrite lastCheck'); } - } else if (next.lastPost?.featureId !== next.checkedIds.at(-1)) { + } else if (next.lastCheck?.featureId !== next.checkedIds.at(-1)) { throw new Error('new progress must checkpoint the newly checked feature'); } return; @@ -325,7 +346,7 @@ function assertValidTransition( next.generation !== prior.generation + 1 || (!allCurrentFeaturesChecked && !retirementResetAllowed) || next.checkedIds.length !== 0 || - next.lastPost || + next.lastCheck || new Date(next.cycleStartedAt) <= new Date(prior.cycleStartedAt) ) { throw new Error('cycle generation reset is invalid'); @@ -519,18 +540,6 @@ function createProgressStore(ctx: WorkforceCtx): ProgressStore { throw new Error('exact Relayfile credentials are required for guardian cycle state'); } -export function featurePostIdempotencyKey(cycleStartedAt: string, featureId: string): string { - return `relay-feature-guardian:${cycleStartedAt}:${featureId}`; -} - -export function deliveredSlackTs(result: WritebackResult | null | undefined): string { - const receipt = result?.receipt as { externalId?: unknown; ts?: unknown } | undefined; - const externalId = typeof receipt?.externalId === 'string' ? receipt.externalId.trim() : ''; - if (isSlackTs(externalId)) return externalId; - const ts = typeof receipt?.ts === 'string' ? receipt.ts.trim() : ''; - return isSlackTs(ts) ? ts : ''; -} - // ── feature selection ───────────────────────────────────────────────────────── function pickNextFeature(features: Feature[], checkedIds: Set): Feature | null { @@ -543,9 +552,9 @@ function pickNextFeature(features: Feature[], checkedIds: Set): Feature return ordered.find((f) => !checkedIds.has(f.id)) ?? null; } -// ── quiz generation ─────────────────────────────────────────────────────────── +// ── check evidence generation ───────────────────────────────────────────────── -async function generateQuizMessage(ctx: WorkforceCtx, feature: Feature): Promise { +async function generateCheckEvidence(ctx: WorkforceCtx, feature: Feature): Promise { const surface = [ feature.cli ? `CLI command: ${feature.cli}` : null, feature.mcp ? `MCP tool: ${feature.mcp}` : null, @@ -564,11 +573,10 @@ async function generateQuizMessage(ctx: WorkforceCtx, feature: Feature): Promise }[feature.tier] ?? 'see feature procedure'; const prompt = [ - 'You are the Relay Feature Guardian, a proactive Slack bot for the Agent Relay team.', - 'Write a brief, conversational Slack message (3-5 sentences, no markdown headers) asking the team to confirm whether a specific feature is working as intended. The feature can be a CLI command or an MCP tool/prompt.', - 'Be specific: name the feature, describe what it should do, show the relevant CLI command or MCP tool/prompt, and ask if it behaves this way or if anything has drifted.', - 'End with: "React ✅ if working as expected, 🔧 if something is off, or ❓ if untested."', - 'Keep it casual and direct — this is an internal team check.', + 'You are the Relay Feature Guardian.', + 'Write concise structured evidence (3-5 sentences, no markdown headers) for an internal success log.', + 'Name the feature, describe its expected behavior, and include the relevant CLI command or MCP tool/prompt.', + 'State the verification tier and criticality. Do not address people, ask for reactions, or claim an external provider check ran.', '', `Feature: ${feature.name}`, surface, @@ -578,39 +586,29 @@ async function generateQuizMessage(ctx: WorkforceCtx, feature: Feature): Promise ].join('\n'); try { - const output = await ctx.llm.complete(prompt, { maxTokens: 300 }); - return output.trim(); - } catch { - return [ - `🔍 *Relay Feature Check: ${feature.name}*`, - ``, - surface, - ``, - `This should: ${feature.desc}`, - ``, - `Is this working as expected right now? React ✅ if yes, 🔧 if something is off, or ❓ if untested.`, - ].join('\n'); + const output = (await ctx.llm.complete(prompt, { maxTokens: 300 })).trim(); + if (output) return output; + ctx.log('warn', 'relay-feature-guardian.evidence-fallback', { + feature: feature.id, + reason: 'model returned empty output', + }); + } catch (err) { + ctx.log('warn', 'relay-feature-guardian.evidence-fallback', { + feature: feature.id, + reason: String(err), + }); } + return [ + `Catalog feature: ${feature.name} (${feature.id}).`, + surface, + `Expected behavior: ${feature.desc}`, + `Verification tier: ${feature.tier} (${tierLabel}); criticality: ${feature.criticality}.`, + ].join('\n'); } // ── agent definition ────────────────────────────────────────────────────────── -export interface GuardianDependencies { - createSlackClient?: typeof slackClient; -} - -export async function runGuardian( - ctx: WorkforceCtx, - _event: WorkforceEvent, - dependencies: GuardianDependencies = {} -): Promise { - const createSlackClient = dependencies.createSlackClient ?? slackClient; - const channel = input(ctx, 'SLACK_CHANNEL'); - if (!channel) { - ctx.log('warn', 'relay-feature-guardian.no-channel', { reason: 'SLACK_CHANNEL not configured' }); - return; - } - +export async function runGuardian(ctx: WorkforceCtx, _event: WorkforceEvent): Promise { // Load the live feature list from the manifest let features: Feature[]; try { @@ -618,14 +616,7 @@ export async function runGuardian( } catch (err) { const absPath = resolveManifestPath(ctx.sandbox.cwd); ctx.log('error', 'relay-feature-guardian.manifest-load-failed', { path: absPath, err: String(err) }); - const isNotFound = String(err).includes('ENOENT'); - const errMsg = isNotFound - ? `⚠️ *relay-feature-guardian* can't find the feature manifest in the cloned relay repository at \`${RELAY_REPO_RELPATH}/${MANIFEST_RELPATH}\`.` - : `⚠️ *relay-feature-guardian* failed to load the feature manifest: \`${String(err)}\``; - await createSlackClient() - .post(channel, errMsg) - .catch(() => undefined); - return; + throw err; } ctx.log('info', 'relay-feature-guardian.manifest-loaded', { path: resolveManifestPath(ctx.sandbox.cwd), @@ -633,13 +624,7 @@ export async function runGuardian( }); if (features.length === 0) { ctx.log('error', 'relay-feature-guardian.no-features', { reason: 'manifest parsed but empty' }); - await createSlackClient() - .post( - channel, - '⚠️ *relay-feature-guardian* loaded the manifest but found no features. Check `.agentworkforce/features/manifest.yaml`.' - ) - .catch(() => undefined); - return; + throw new Error('relay-feature-guardian loaded an empty feature manifest'); } const totalFeatures = features.length; @@ -650,15 +635,15 @@ export async function runGuardian( progress = await store.load(features); } catch (err) { ctx.log('error', 'relay-feature-guardian.progress-load-failed', { err: String(err) }); - return; + throw err; } // A missing exact record is the only bootstrap case. Persist and read back - // the empty generation before any Slack side effect. + // the empty generation before the feature exercise. if (!progress) { const initial: ProgressState = { kind: 'relay-feature-guardian:progress', - version: 3, + version: 4, generation: 1, checkedIds: [], cycleStartedAt: new Date().toISOString(), @@ -668,7 +653,7 @@ export async function runGuardian( progress = await store.save(initial, null, features); } catch (err) { ctx.log('error', 'relay-feature-guardian.cycle-checkpoint-failed', { err: String(err) }); - return; + throw err; } } @@ -681,17 +666,17 @@ export async function runGuardian( currentTotal: totalFeatures, retiredIds, }); - return; + throw new Error(manifestDelta.reason); } // A successfully parsed manifest can retire a feature mid-cycle. Historical // IDs are accepted only at load; reset them under exact-revision CAS before - // any Slack side effect so the persisted state is canonical again. + // the feature exercise so the persisted state is canonical again. if (manifestDelta.kind === 'reset-checked-retirement') { const previousStart = new Date(progress.state.cycleStartedAt).valueOf(); const reset: ProgressState = { kind: 'relay-feature-guardian:progress', - version: 3, + version: 4, generation: progress.state.generation + 1, checkedIds: [], cycleStartedAt: new Date(Math.max(Date.now(), previousStart + 1)).toISOString(), @@ -706,11 +691,11 @@ export async function runGuardian( }); } catch (err) { ctx.log('error', 'relay-feature-guardian.progress-reconcile-failed', { err: String(err) }); - return; + throw err; } } else if (progress.state.totalFeatures !== totalFeatures) { // Manifest additions change the denominator but must never discard already - // checked feature ids or overwrite the last delivered receipt. + // checked feature ids or overwrite the latest check evidence. const reconciled: ProgressState = { ...progress.state, totalFeatures, @@ -719,7 +704,7 @@ export async function runGuardian( progress = await store.save(reconciled, progress, features); } catch (err) { ctx.log('error', 'relay-feature-guardian.progress-reconcile-failed', { err: String(err) }); - return; + throw err; } } @@ -732,7 +717,7 @@ export async function runGuardian( const previousStart = new Date(progress.state.cycleStartedAt).valueOf(); const reset: ProgressState = { kind: 'relay-feature-guardian:progress', - version: 3, + version: 4, generation: progress.state.generation + 1, checkedIds: [], cycleStartedAt: new Date(Math.max(Date.now(), previousStart + 1)).toISOString(), @@ -742,96 +727,44 @@ export async function runGuardian( progress = await store.save(reset, progress, features); } catch (err) { ctx.log('error', 'relay-feature-guardian.cycle-checkpoint-failed', { err: String(err) }); - return; + throw err; } checkedIds = new Set(); feature = pickNextFeature(features, checkedIds); } - if (!feature) return; + if (!feature) throw new Error('relay-feature-guardian could not select a feature'); - // Build @mention string - const userWill = input(ctx, 'SLACK_USER_WILL'); - const userKhaliq = input(ctx, 'SLACK_USER_KHALIQ'); - const mentions = [userWill && `<@${userWill}>`, userKhaliq && `<@${userKhaliq}>`].filter(Boolean).join(' '); - const mentionPrefix = mentions ? `${mentions} — ` : ''; - - // Generate quiz message - const quizBody = await generateQuizMessage(ctx, feature); + // Exercise the catalog + model chain without writing to a human channel. + const evidence = await generateCheckEvidence(ctx, feature); const remaining = totalFeatures - checkedIds.size - 1; - const progressNote = `_[relay feature check · ${checkedIds.size + 1}/${totalFeatures} · ${remaining} remaining in cycle]_`; - const message = [mentionPrefix + quizBody, '', progressNote].join('\n'); - ctx.log('info', 'relay-feature-guardian.posting', { - channel, - feature: feature.id, - index: checkedIds.size + 1, - total: totalFeatures, - remaining, - }); - - // Post to Slack - const slack = createSlackClient({ - writebackTimeoutMs: SLACK_WRITEBACK_TIMEOUT_MS, - writebackPollMs: SLACK_WRITEBACK_POLL_MS, - }); - let result: WritebackResult; - try { - result = await slack.messages.write( - { channelId: channel }, - { - text: message, - idempotencyKey: featurePostIdempotencyKey(progress.state.cycleStartedAt, feature.id), - } - ); - } catch (err) { - ctx.log('error', 'relay-feature-guardian.post-failed', { - channel, - feature: feature.id, - err: String(err), - }); - throw err; - } - const ts = deliveredSlackTs(result); - if (!ts) { - // A successful helper return means the draft was admitted, not that the - // provider receipt is already visible. Leave the exact checkpoint alone so - // the next tick replays the stable idempotency key instead of turning an - // eventually-consistent receipt into a terminal handler failure. - ctx.log('warn', 'relay-feature-guardian.post-receipt-pending', { - channel, - feature: feature.id, - path: result.path, - }); - return; - } - - // Checkpoint immediately after the confirmed provider receipt. The stable - // idempotency key makes a retry safe if this save times out or the run caps. + const checkedAt = new Date().toISOString(); checkedIds.add(feature.id); const completed: ProgressState = { kind: 'relay-feature-guardian:progress', - version: 3, + version: 4, generation: progress.state.generation, checkedIds: [...checkedIds], cycleStartedAt: progress.state.cycleStartedAt, totalFeatures, - lastPost: { featureId: feature.id, ts }, + lastCheck: { featureId: feature.id, checkedAt, evidence: 'log-only' }, }; let checkpoint: ProgressSnapshot; try { checkpoint = await store.save(completed, progress, features); } catch (err) { ctx.log('error', 'relay-feature-guardian.progress-checkpoint-failed', { - channel, feature: feature.id, - ts, err: String(err), }); - return; + throw err; } - ctx.log('info', 'relay-feature-guardian.posted', { - channel, + ctx.log('info', 'relay-feature-guardian.catalog-traversal-passed', { feature: feature.id, - ts, + checkedAt, + evidence, + index: checkedIds.size, + total: totalFeatures, + remaining, checkpointRevision: checkpoint.revision, }); } diff --git a/.agentworkforce/agents/relay-feature-guardian/persona.json b/.agentworkforce/agents/relay-feature-guardian/persona.json index 034e76cbd..bb92988dd 100644 --- a/.agentworkforce/agents/relay-feature-guardian/persona.json +++ b/.agentworkforce/agents/relay-feature-guardian/persona.json @@ -2,7 +2,7 @@ "id": "relay-feature-guardian", "intent": "relay-orchestrator", "tags": ["relay", "verification", "proactive", "health"], - "description": "Cycles through every Relay CLI feature hourly, posting a Slack check asking the team to confirm whether the feature works as intended. Tracks progress across the full manifest and resets after a complete cycle.", + "description": "Traverses the Relay feature catalog hourly to exercise the manifest/model/state chain, recording healthy traversal as log-only evidence without claiming per-feature E2E verification; genuine run failures surface through deployment monitoring.", "cloud": true, "harness": "opencode", "model": "deepseek-v4-flash-free", @@ -19,48 +19,9 @@ "requiredReadPaths": ["/github/repos/AgentWorkforce/relay/.agentworkforce/features/**"], "writeOnlyPaths": [] } - }, - "slack": { - "optional": true, - "enabledByInput": "SLACK_CHANNEL", - "scope": { - "paths": "/slack/channels/**" - }, - "relayfileMount": { - "requiredReadPaths": [], - "writeOnlyPaths": ["/slack/channels/${SLACK_CHANNEL}/**"] - } - } - }, - "inputs": { - "SLACK_CHANNEL": { - "description": "Slack channel to post feature checks to (e.g. relay-health or general).", - "env": "SLACK_CHANNEL", - "default": "C0AEKNLDNKW", - "picker": { - "provider": "slack", - "resource": "channels" - } - }, - "SLACK_USER_WILL": { - "description": "Slack user ID for Will. Used for @mentions in feature checks.", - "env": "SLACK_USER_WILL", - "optional": true, - "picker": { - "provider": "slack", - "resource": "users" - } - }, - "SLACK_USER_KHALIQ": { - "description": "Slack user ID for Khaliq. Used for @mentions in feature checks.", - "env": "SLACK_USER_KHALIQ", - "optional": true, - "picker": { - "provider": "slack", - "resource": "users" - } } }, + "inputs": {}, "memory": { "enabled": true, "scopes": ["workspace"],