1818 */
1919
2020import * as crypto from 'crypto' ;
21- import { BedrockRuntimeClient } from '@aws-sdk/client-bedrock-runtime' ;
21+ import { BedrockRuntimeClient , ApplyGuardrailCommand } from '@aws-sdk/client-bedrock-runtime' ;
2222import { DynamoDBClient } from '@aws-sdk/client-dynamodb' ;
2323import { S3Client } from '@aws-sdk/client-s3' ;
2424import { DynamoDBDocumentClient , GetCommand , UpdateCommand } from '@aws-sdk/lib-dynamodb' ;
@@ -30,6 +30,7 @@ import { renderMaturingReply } from './shared/iteration-reply';
3030import { cleanupPreScreenedAttachments , downloadScreenAndStoreLinearAttachments , LinearAttachmentError } from './shared/linear-attachments' ;
3131import {
3232 deleteComment ,
33+ fetchRecentComments ,
3334 reactToComment ,
3435 replyToComment ,
3536 reportIssueFailure ,
@@ -41,6 +42,7 @@ import {
4142 EMOJI_STARTED ,
4243 EMOJI_SUCCESS ,
4344 EMOJI_NEEDS_INPUT ,
45+ type RenderedComment ,
4446} from './shared/linear-feedback' ;
4547import {
4648 probeLinearIssueContext ,
@@ -113,6 +115,7 @@ import { readConcurrencyBudget, releaseReadyChildren } from './shared/orchestrat
113115import { upsertEpicPanel } from './shared/orchestration-rollup' ;
114116import { claimCommentAck , clearRollupClaim , deriveOrchestrationId , loadOrchestration , setStatusCommentId , type OrchestrationReleaseContext } from './shared/orchestration-store' ;
115117import type { Attachment , PassedAttachmentRecord } from './shared/types' ;
118+ import { MAX_TASK_DESCRIPTION_LENGTH } from './shared/validation' ;
116119import { CODING_WORKFLOW_ID } from './shared/workflows' ;
117120
118121const ddb = DynamoDBDocumentClient . from ( new DynamoDBClient ( { } ) ) ;
@@ -900,7 +903,22 @@ export async function handler(event: ProcessorEvent): Promise<void> {
900903 }
901904 }
902905
903- const taskDescription = buildTaskDescription ( issue , contextHint ) ;
906+ // ADR-016 pre-hydration: fetch recent HUMAN comments and fold them into the
907+ // task description — the agent has no Linear MCP to read the thread at
908+ // runtime. Advisory + fail-open end to end: a fetch failure yields no
909+ // comments, and third-party comment text that trips the guardrail is dropped
910+ // (never the task; the reporter-authored description is screened separately by
911+ // createTaskCore). Mirrors the Jira processor (#619/#577).
912+ let recentComments : RenderedComment [ ] = [ ] ;
913+ if ( WORKSPACE_REGISTRY_TABLE && resolvedAccessToken ) {
914+ const fetched = await fetchRecentComments (
915+ { linearWorkspaceId : workspaceId , registryTableName : WORKSPACE_REGISTRY_TABLE } ,
916+ issue . id ,
917+ ) ;
918+ recentComments = await screenCommentsOrDrop ( fetched , issue . id , workspaceId ) ;
919+ }
920+
921+ const taskDescription = buildTaskDescription ( issue , contextHint , recentComments ) ;
904922
905923 // Extract embedded image URLs from the issue description markdown. Non-Linear
906924 // (public CDN) images become URL attachments fetched+screened during context
@@ -3021,7 +3039,11 @@ function buildRevisionTaskDescription(issueText: string, pending: PendingPlan, f
30213039 ] . join ( '\n' ) ;
30223040}
30233041
3024- function buildTaskDescription ( issue : LinearIssueEvent [ 'data' ] , contextHint : string = '' ) : string {
3042+ function buildTaskDescription (
3043+ issue : LinearIssueEvent [ 'data' ] ,
3044+ contextHint : string = '' ,
3045+ comments : readonly RenderedComment [ ] = [ ] ,
3046+ ) : string {
30253047 const parts : string [ ] = [ ] ;
30263048 if ( issue . identifier && issue . title ) {
30273049 parts . push ( `${ issue . identifier } : ${ issue . title } ` ) ;
@@ -3036,7 +3058,99 @@ function buildTaskDescription(issue: LinearIssueEvent['data'], contextHint: stri
30363058 parts . push ( '' ) ;
30373059 parts . push ( issue . description . trim ( ) ) ;
30383060 }
3039- return parts . join ( '\n' ) || 'Linear issue' ;
3061+ const core = parts . join ( '\n' ) || 'Linear issue' ;
3062+
3063+ // Fold recent human comments under a clear heading so the agent can tell them
3064+ // from the description (ADR-016 pre-hydration). Comments are ADVISORY and must
3065+ // stay fail-open: they must never grow the description past
3066+ // MAX_TASK_DESCRIPTION_LENGTH and turn createTaskCore's length check into a
3067+ // hard rejection. Only append what fits the remaining budget, truncating the
3068+ // section if needed. Mirrors the Jira processor.
3069+ if ( comments . length === 0 ) return core ;
3070+ const commentSection = renderCommentSection ( comments ) ;
3071+ const separator = '\n' ;
3072+ const budget = MAX_TASK_DESCRIPTION_LENGTH - core . length - separator . length ;
3073+ if ( budget <= 0 ) return core ; // description already fills the budget — drop comments
3074+ const fitted = commentSection . length <= budget
3075+ ? commentSection
3076+ : truncateCommentSection ( commentSection , budget ) ;
3077+ return fitted ? core + separator + fitted : core ;
3078+ }
3079+
3080+ /** Notice appended when the comment section is truncated to fit the budget. */
3081+ const COMMENT_TRUNCATION_NOTICE = '\n\n_(recent comments truncated)_' ;
3082+
3083+ function renderCommentSection ( comments : readonly RenderedComment [ ] ) : string {
3084+ const lines : string [ ] = [ '' , '## Recent comments' ] ;
3085+ for ( const c of comments ) {
3086+ lines . push ( '' ) ;
3087+ const attribution = c . createdAt ? `**${ c . author } ** (${ c . createdAt } ):` : `**${ c . author } **:` ;
3088+ lines . push ( attribution ) ;
3089+ lines . push ( c . markdown ) ;
3090+ }
3091+ return lines . join ( '\n' ) ;
3092+ }
3093+
3094+ /**
3095+ * Trim a rendered comment section to at most ``budget`` characters, leaving room
3096+ * for a truncation notice. Returns '' if even the heading + notice can't fit, so
3097+ * the caller cleanly drops the section. Mirrors the Jira processor.
3098+ */
3099+ function truncateCommentSection ( section : string , budget : number ) : string {
3100+ const room = budget - COMMENT_TRUNCATION_NOTICE . length ;
3101+ if ( room <= 0 ) return '' ;
3102+ return section . slice ( 0 , room ) + COMMENT_TRUNCATION_NOTICE ;
3103+ }
3104+
3105+ /**
3106+ * Screen the rendered comment block through the Bedrock Guardrail on its own, so
3107+ * third-party comment content that trips the policy is DROPPED (fail-open)
3108+ * rather than gating the reporter's task. Returns the comments unchanged when
3109+ * they pass, and ``[]`` when the guardrail intervenes or is unavailable — the
3110+ * task still proceeds with the reporter-authored title/description (which
3111+ * createTaskCore screens separately). Keeps the comment-enrichment contract
3112+ * fail-open end to end. Mirrors the Jira processor (#577 review, item 4).
3113+ */
3114+ async function screenCommentsOrDrop (
3115+ comments : RenderedComment [ ] ,
3116+ issueId : string ,
3117+ workspaceId : string ,
3118+ ) : Promise < RenderedComment [ ] > {
3119+ if ( comments . length === 0 ) return comments ;
3120+ if ( ! attachmentsBedrockClient || ! GUARDRAIL_ID || ! GUARDRAIL_VERSION ) {
3121+ // No guardrail configured — drop unscreened third-party text rather than
3122+ // route it, unscreened, into the agent context.
3123+ logger . warn ( 'Dropping Linear comments: guardrail not configured to screen them' , {
3124+ issue_id : issueId ,
3125+ linear_workspace_id : workspaceId ,
3126+ } ) ;
3127+ return [ ] ;
3128+ }
3129+ const text = renderCommentSection ( comments ) ;
3130+ try {
3131+ const result = await attachmentsBedrockClient . send ( new ApplyGuardrailCommand ( {
3132+ guardrailIdentifier : GUARDRAIL_ID ,
3133+ guardrailVersion : GUARDRAIL_VERSION ,
3134+ source : 'INPUT' ,
3135+ content : [ { text : { text } } ] ,
3136+ } ) ) ;
3137+ if ( result . action === 'GUARDRAIL_INTERVENED' ) {
3138+ logger . warn ( 'Dropping Linear comments: blocked by content policy (task still proceeds)' , {
3139+ issue_id : issueId ,
3140+ linear_workspace_id : workspaceId ,
3141+ } ) ;
3142+ return [ ] ;
3143+ }
3144+ return comments ;
3145+ } catch ( err ) {
3146+ // Fail-open on a screening outage too — comments are advisory.
3147+ logger . warn ( 'Dropping Linear comments: screening unavailable (task still proceeds)' , {
3148+ issue_id : issueId ,
3149+ linear_workspace_id : workspaceId ,
3150+ error : err instanceof Error ? err . message : String ( err ) ,
3151+ } ) ;
3152+ return [ ] ;
3153+ }
30403154}
30413155
30423156/**
0 commit comments