Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/config/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ describe('FactoryConfigSchema', () => {
channel: 'C123',
style: 'threaded-summarized',
botUserId: 'U0B2596R7EZ',
stakeholderUserIds: [],
staleAfterMs: 10 * 60_000,
})
expect(parsed.mergePolicy).toBe('never')
Expand Down
4 changes: 4 additions & 0 deletions src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ const slackSchema = z.object({
channel: z.string(),
style: z.literal('threaded-summarized').default('threaded-summarized'),
botUserId: z.string().default('U0B2596R7EZ'),
// Slack user IDs to mention when an agent needs a human decision. Keeping
// this explicit avoids guessing that a Linear assignee name is also a Slack
// identity, while still making parked questions immediately actionable.
stakeholderUserIds: z.array(z.string().min(1)).default([]),
staleAfterMs: z.number().int().min(1_000).default(10 * 60_000),
}).optional()

Expand Down
17 changes: 7 additions & 10 deletions src/dispatch/templates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('renderAgentTask', () => {
expect(task).toContain('Factory will open the PR targeting the repository default branch through the connected GitHub workspace.')
expect(task).toContain('Do not run `gh pr create` or require local GitHub CLI authentication.')
expect(task).toContain('Factory will hand the opened PR to reviewer `ar-123-review`.')
expect(task).toContain('write to the .integrations mount path so the factory can relay it to the issue thread.')
expect(task).toContain('DM `factory` with `[factory-needs-input]`')
expect(task).toContain('DM `broker` when fully done.')
expect(task).toContain('Do NOT auto-merge.')
expect(task).toContain('Merge policy: never')
Expand Down Expand Up @@ -238,7 +238,7 @@ describe('renderAgentTask', () => {
expect(task).not.toMatch(/\n\n(.integrations|Connected integrations)/m)
})

it('directs agent questions to .integrations writeback instead of relay DM', () => {
it('makes the needs-input marker an explicit release boundary', () => {
const task = renderAgentTask({
issue,
route: { repo: 'pear', clonePath: '/tmp/pear' },
Expand All @@ -248,14 +248,11 @@ describe('renderAgentTask', () => {
slackDispatchThread: { channel: 'C123', threadId: '169.000', mountRoot: '/work/.integrations' },
})

// The Slack-thread writeback replaces the old relay DM pattern.
expect(task).toContain('write your question to this issue\'s Slack dispatch thread via the .integrations mount')
expect(task).toContain('Write path: /work/.integrations/slack/channels/C123/messages/169_000/replies/question.json')
expect(task).toContain('Write a JSON object with a "text" field')
expect(task).toContain('Continue with safe reversible work while waiting for a reply.')
// No relay DM or legacy patterns.
expect(task).toContain('DM `factory` with `[factory-needs-input]`')
expect(task).toContain('/work/.integrations/slack/channels/C123/messages/169_000/replies/question.json')
expect(task).toContain('Do not wait or poll')
expect(task).toContain('release the whole team and resume it from session memory only after the first human reply')
expect(task).toContain('cold-start the team with the issue, question, reply, branch, and PR context')
expect(task).not.toContain('FACTORY_NEEDS_INPUT')
expect(task).not.toContain('DM `broker` with')
expect(task).not.toContain('[factory-needs-input]')
})
})
11 changes: 5 additions & 6 deletions src/dispatch/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,21 +90,20 @@ export function renderAgentTask(input: RenderAgentTaskInput): string {
'When implementation is complete, finish your session normally; Factory will open the PR targeting the repository default branch through the connected GitHub workspace.',
'Do not run `gh pr create` or require local GitHub CLI authentication.',
`Factory will hand the opened PR to reviewer \`${input.reviewerName}\`.`,
'If blocked and you need human input, write to the .integrations mount path so the factory can relay it to the issue thread.',
'If blocked and you need human input, DM `factory` with `[factory-needs-input]`, the issue key, and one concrete question. Factory will relay it to the issue conversation.',
'DM `broker` when fully done.',
'Do NOT auto-merge.',
mergePolicyLine(input.config.mergePolicy),
]
const questionInstructions = input.slackDispatchThread
? [
'',
'If you are blocked or need a human answer mid-task, write your question to this issue\'s Slack dispatch thread via the .integrations mount.',
'If you are blocked or need a human answer mid-task, finish any safe reversible work first, then DM `factory` with `[factory-needs-input]`, the issue key, and one concrete question.',
// Absolute path: the agent runs in its repo clone, not the daemon cwd
// where .integrations lives, so a relative path would be unreachable.
`Write path: ${input.slackDispatchThread.mountRoot}/slack/channels/${input.slackDispatchThread.channel}/messages/${input.slackDispatchThread.threadId.replaceAll('.', '_')}/replies/question.json`,
'Write a JSON object with a "text" field containing your question.',
'The human\'s reply will be delivered to you as an `<integration-event>` system message injected into your session — wait for it, do not poll.',
'Continue with safe reversible work while waiting for a reply.',
`Factory will durably post and, if necessary, retry the question to the Slack thread represented at ${input.slackDispatchThread.mountRoot}/slack/channels/${input.slackDispatchThread.channel}/messages/${input.slackDispatchThread.threadId.replaceAll('.', '_')}/replies/question.json.`,
'After sending the marker, stop work and finish your session normally. Do not wait or poll: Factory will release the whole team and resume it from session memory only after the first human reply.',
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
'If session resume is unavailable, Factory will cold-start the team with the issue, question, reply, branch, and PR context so work can be re-hydrated explicitly.',
]
: []

Expand Down
Loading