agentic-loop 3.10.2 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,408 @@
1
+ ---
2
+ description: AWS patterns and best practices. Use when working with S3, Lambda, DynamoDB, IAM, SQS, or other AWS services.
3
+ ---
4
+
5
+ # AWS Patterns
6
+
7
+ Best practices for AWS services. Use these patterns when writing infrastructure or application code that interacts with AWS.
8
+
9
+ ## IAM
10
+
11
+ ### Least Privilege
12
+
13
+ ```json
14
+ // ❌ NEVER: Wildcard permissions
15
+ {
16
+ "Effect": "Allow",
17
+ "Action": "s3:*",
18
+ "Resource": "*"
19
+ }
20
+
21
+ // ✅ ALWAYS: Specific actions and resources
22
+ {
23
+ "Effect": "Allow",
24
+ "Action": ["s3:GetObject", "s3:PutObject"],
25
+ "Resource": "arn:aws:s3:::my-bucket/uploads/*"
26
+ }
27
+ ```
28
+
29
+ ### Service Roles
30
+
31
+ ```typescript
32
+ // ❌ NEVER: Use root credentials or long-lived keys
33
+ const client = new S3Client({
34
+ credentials: {
35
+ accessKeyId: 'AKIA...',
36
+ secretAccessKey: '...'
37
+ }
38
+ });
39
+
40
+ // ✅ ALWAYS: Use IAM roles (SDK auto-discovers)
41
+ const client = new S3Client({ region: 'us-east-1' });
42
+ // In Lambda/ECS/EC2 - credentials come from execution role
43
+ ```
44
+
45
+ ### Cross-Account Access
46
+
47
+ ```json
48
+ // Trust policy for cross-account role assumption
49
+ {
50
+ "Version": "2012-10-17",
51
+ "Statement": [{
52
+ "Effect": "Allow",
53
+ "Principal": {
54
+ "AWS": "arn:aws:iam::OTHER_ACCOUNT:role/service-role"
55
+ },
56
+ "Action": "sts:AssumeRole",
57
+ "Condition": {
58
+ "StringEquals": {
59
+ "sts:ExternalId": "${external_id}" // Prevent confused deputy
60
+ }
61
+ }
62
+ }]
63
+ }
64
+ ```
65
+
66
+ ## S3
67
+
68
+ ### Secure Defaults
69
+
70
+ ```typescript
71
+ // ✅ Create buckets with secure defaults
72
+ const bucket = new s3.Bucket(this, 'Bucket', {
73
+ blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
74
+ encryption: s3.BucketEncryption.S3_MANAGED,
75
+ enforceSSL: true,
76
+ versioned: true,
77
+ removalPolicy: RemovalPolicy.RETAIN // Don't delete data
78
+ });
79
+ ```
80
+
81
+ ### Presigned URLs
82
+
83
+ ```typescript
84
+ // ✅ For temporary access without exposing credentials
85
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
86
+ import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
87
+
88
+ // Download URL (15 min expiry)
89
+ const downloadUrl = await getSignedUrl(s3Client,
90
+ new GetObjectCommand({ Bucket: bucket, Key: key }),
91
+ { expiresIn: 900 }
92
+ );
93
+
94
+ // Upload URL with constraints
95
+ const uploadUrl = await getSignedUrl(s3Client,
96
+ new PutObjectCommand({
97
+ Bucket: bucket,
98
+ Key: `uploads/${userId}/${filename}`,
99
+ ContentType: 'image/jpeg' // Restrict file type
100
+ }),
101
+ { expiresIn: 300 }
102
+ );
103
+ ```
104
+
105
+ ### Multipart Uploads
106
+
107
+ ```typescript
108
+ // ✅ For files > 100MB
109
+ import { Upload } from '@aws-sdk/lib-storage';
110
+
111
+ const upload = new Upload({
112
+ client: s3Client,
113
+ params: { Bucket: bucket, Key: key, Body: stream },
114
+ partSize: 10 * 1024 * 1024, // 10MB parts
115
+ leavePartsOnError: false // Clean up on failure
116
+ });
117
+
118
+ upload.on('httpUploadProgress', (progress) => {
119
+ console.log(`${progress.loaded}/${progress.total}`);
120
+ });
121
+
122
+ await upload.done();
123
+ ```
124
+
125
+ ## Lambda
126
+
127
+ ### Handler Pattern
128
+
129
+ ```typescript
130
+ // ✅ Initialize outside handler (reused across invocations)
131
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
132
+
133
+ const ddb = new DynamoDBClient({}); // Cold start only
134
+
135
+ export const handler = async (event: APIGatewayEvent) => {
136
+ // Handler code uses pre-initialized client
137
+ try {
138
+ const result = await processEvent(event);
139
+ return { statusCode: 200, body: JSON.stringify(result) };
140
+ } catch (error) {
141
+ console.error('Handler error:', error); // CloudWatch logging
142
+ return { statusCode: 500, body: JSON.stringify({ error: 'Internal error' }) };
143
+ }
144
+ };
145
+ ```
146
+
147
+ ### Environment Variables
148
+
149
+ ```typescript
150
+ // ✅ Validate at cold start, not per-request
151
+ const TABLE_NAME = process.env.TABLE_NAME;
152
+ const QUEUE_URL = process.env.QUEUE_URL;
153
+
154
+ if (!TABLE_NAME || !QUEUE_URL) {
155
+ throw new Error('Missing required environment variables');
156
+ }
157
+
158
+ export const handler = async (event) => {
159
+ // Use TABLE_NAME, QUEUE_URL directly
160
+ };
161
+ ```
162
+
163
+ ### Timeouts and Memory
164
+
165
+ ```typescript
166
+ // ✅ CDK configuration
167
+ new lambda.Function(this, 'Handler', {
168
+ runtime: lambda.Runtime.NODEJS_20_X,
169
+ handler: 'index.handler',
170
+ code: lambda.Code.fromAsset('dist'),
171
+ timeout: Duration.seconds(30), // Always set explicitly
172
+ memorySize: 256, // More memory = more CPU
173
+ reservedConcurrentExecutions: 10, // Prevent runaway scaling
174
+ environment: {
175
+ TABLE_NAME: table.tableName,
176
+ NODE_OPTIONS: '--enable-source-maps'
177
+ }
178
+ });
179
+ ```
180
+
181
+ ### Dead Letter Queues
182
+
183
+ ```typescript
184
+ // ✅ Capture failed invocations
185
+ const dlq = new sqs.Queue(this, 'DLQ', {
186
+ retentionPeriod: Duration.days(14)
187
+ });
188
+
189
+ new lambda.Function(this, 'Handler', {
190
+ // ...
191
+ deadLetterQueue: dlq,
192
+ retryAttempts: 2
193
+ });
194
+ ```
195
+
196
+ ## DynamoDB
197
+
198
+ ### Table Design
199
+
200
+ ```typescript
201
+ // ✅ Single-table design with composite keys
202
+ const table = new dynamodb.Table(this, 'Table', {
203
+ partitionKey: { name: 'PK', type: dynamodb.AttributeType.STRING },
204
+ sortKey: { name: 'SK', type: dynamodb.AttributeType.STRING },
205
+ billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
206
+ pointInTimeRecovery: true,
207
+ encryption: dynamodb.TableEncryption.AWS_MANAGED
208
+ });
209
+
210
+ // Add GSI for access patterns
211
+ table.addGlobalSecondaryIndex({
212
+ indexName: 'GSI1',
213
+ partitionKey: { name: 'GSI1PK', type: dynamodb.AttributeType.STRING },
214
+ sortKey: { name: 'GSI1SK', type: dynamodb.AttributeType.STRING }
215
+ });
216
+ ```
217
+
218
+ ### Key Patterns
219
+
220
+ ```typescript
221
+ // ✅ Composite keys for flexible queries
222
+ // User: PK=USER#123, SK=PROFILE
223
+ // User's orders: PK=USER#123, SK=ORDER#2024-01-15#456
224
+ // Order by ID: GSI1PK=ORDER#456, GSI1SK=ORDER#456
225
+
226
+ await ddb.put({
227
+ TableName: TABLE_NAME,
228
+ Item: {
229
+ PK: `USER#${userId}`,
230
+ SK: `ORDER#${date}#${orderId}`,
231
+ GSI1PK: `ORDER#${orderId}`,
232
+ GSI1SK: `ORDER#${orderId}`,
233
+ // ... other attributes
234
+ }
235
+ });
236
+
237
+ // Query user's recent orders
238
+ await ddb.query({
239
+ TableName: TABLE_NAME,
240
+ KeyConditionExpression: 'PK = :pk AND begins_with(SK, :sk)',
241
+ ExpressionAttributeValues: {
242
+ ':pk': `USER#${userId}`,
243
+ ':sk': 'ORDER#'
244
+ },
245
+ ScanIndexForward: false, // Newest first
246
+ Limit: 10
247
+ });
248
+ ```
249
+
250
+ ### Transactions
251
+
252
+ ```typescript
253
+ // ✅ Atomic operations across items
254
+ await ddb.transactWrite({
255
+ TransactItems: [
256
+ {
257
+ Update: {
258
+ TableName: TABLE_NAME,
259
+ Key: { PK: `USER#${userId}`, SK: 'BALANCE' },
260
+ UpdateExpression: 'SET balance = balance - :amount',
261
+ ConditionExpression: 'balance >= :amount',
262
+ ExpressionAttributeValues: { ':amount': amount }
263
+ }
264
+ },
265
+ {
266
+ Put: {
267
+ TableName: TABLE_NAME,
268
+ Item: {
269
+ PK: `USER#${userId}`,
270
+ SK: `TXN#${txnId}`,
271
+ amount,
272
+ timestamp: Date.now()
273
+ }
274
+ }
275
+ }
276
+ ]
277
+ });
278
+ ```
279
+
280
+ ## SQS
281
+
282
+ ### Producer Pattern
283
+
284
+ ```typescript
285
+ // ✅ Batch sends for efficiency
286
+ import { SendMessageBatchCommand } from '@aws-sdk/client-sqs';
287
+
288
+ const entries = items.map((item, i) => ({
289
+ Id: String(i),
290
+ MessageBody: JSON.stringify(item),
291
+ MessageGroupId: item.groupId, // For FIFO queues
292
+ MessageDeduplicationId: item.id // For FIFO queues
293
+ }));
294
+
295
+ // Send in batches of 10 (SQS limit)
296
+ for (let i = 0; i < entries.length; i += 10) {
297
+ await sqs.send(new SendMessageBatchCommand({
298
+ QueueUrl: QUEUE_URL,
299
+ Entries: entries.slice(i, i + 10)
300
+ }));
301
+ }
302
+ ```
303
+
304
+ ### Consumer Pattern
305
+
306
+ ```typescript
307
+ // ✅ Lambda SQS trigger with partial batch response
308
+ export const handler = async (event: SQSEvent) => {
309
+ const failures: SQSBatchItemFailure[] = [];
310
+
311
+ for (const record of event.Records) {
312
+ try {
313
+ const body = JSON.parse(record.body);
314
+ await processMessage(body);
315
+ } catch (error) {
316
+ console.error(`Failed: ${record.messageId}`, error);
317
+ failures.push({ itemIdentifier: record.messageId });
318
+ }
319
+ }
320
+
321
+ return { batchItemFailures: failures }; // Only retry failed messages
322
+ };
323
+ ```
324
+
325
+ ## Secrets Manager
326
+
327
+ ```typescript
328
+ // ✅ Cache secrets, don't fetch per-request
329
+ import { GetSecretValueCommand, SecretsManagerClient } from '@aws-sdk/client-secrets-manager';
330
+
331
+ const sm = new SecretsManagerClient({});
332
+ let cachedSecret: string | null = null;
333
+
334
+ async function getSecret(): Promise<string> {
335
+ if (!cachedSecret) {
336
+ const response = await sm.send(new GetSecretValueCommand({
337
+ SecretId: process.env.SECRET_ARN
338
+ }));
339
+ cachedSecret = response.SecretString!;
340
+ }
341
+ return cachedSecret;
342
+ }
343
+ ```
344
+
345
+ ## Error Handling
346
+
347
+ ```typescript
348
+ // ✅ Handle AWS-specific errors
349
+ import { ConditionalCheckFailedException } from '@aws-sdk/client-dynamodb';
350
+ import { S3ServiceException } from '@aws-sdk/client-s3';
351
+
352
+ try {
353
+ await ddb.put({ /* ... */, ConditionExpression: 'attribute_not_exists(PK)' });
354
+ } catch (error) {
355
+ if (error instanceof ConditionalCheckFailedException) {
356
+ throw new ConflictError('Item already exists');
357
+ }
358
+ throw error;
359
+ }
360
+
361
+ try {
362
+ await s3.getObject({ Bucket, Key });
363
+ } catch (error) {
364
+ if (error instanceof S3ServiceException && error.name === 'NoSuchKey') {
365
+ return null;
366
+ }
367
+ throw error;
368
+ }
369
+ ```
370
+
371
+ ## Cost Optimization
372
+
373
+ ```typescript
374
+ // ✅ Use appropriate storage classes
375
+ new s3.Bucket(this, 'Bucket', {
376
+ lifecycleRules: [{
377
+ transitions: [
378
+ { storageClass: s3.StorageClass.INFREQUENT_ACCESS, transitionAfter: Duration.days(30) },
379
+ { storageClass: s3.StorageClass.GLACIER, transitionAfter: Duration.days(90) }
380
+ ],
381
+ expiration: Duration.days(365)
382
+ }]
383
+ });
384
+
385
+ // ✅ DynamoDB on-demand for unpredictable workloads
386
+ billingMode: dynamodb.BillingMode.PAY_PER_REQUEST
387
+
388
+ // ✅ Lambda ARM64 for better price/performance
389
+ architecture: lambda.Architecture.ARM_64
390
+ ```
391
+
392
+ ## Testing
393
+
394
+ ```typescript
395
+ // ✅ Use local emulators for unit tests
396
+ // docker run -p 8000:8000 amazon/dynamodb-local
397
+
398
+ const ddb = new DynamoDBClient({
399
+ endpoint: process.env.DYNAMODB_ENDPOINT || undefined, // Local in tests
400
+ region: 'us-east-1'
401
+ });
402
+
403
+ // ✅ Or use mocks
404
+ import { mockClient } from 'aws-sdk-client-mock';
405
+
406
+ const ddbMock = mockClient(DynamoDBClient);
407
+ ddbMock.on(GetItemCommand).resolves({ Item: { /* ... */ } });
408
+ ```
package/bin/ralph.sh CHANGED
@@ -87,7 +87,8 @@ source "$RALPH_LIB/utils.sh"
87
87
  source "$RALPH_LIB/init.sh"
88
88
  source "$RALPH_LIB/setup.sh"
89
89
  source "$RALPH_LIB/loop.sh"
90
- source "$RALPH_LIB/verify.sh"
90
+ source "$RALPH_LIB/prd-check.sh" # PRD validation before loop
91
+ source "$RALPH_LIB/code-check.sh" # Code verification after Claude writes
91
92
  source "$RALPH_LIB/prd.sh"
92
93
  source "$RALPH_LIB/signs.sh"
93
94
  source "$RALPH_LIB/test.sh"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-loop",
3
- "version": "3.10.2",
3
+ "version": "3.11.0",
4
4
  "description": "Autonomous AI coding loop - PRD-driven development with Claude Code",
5
5
  "author": "Allie Jones <allie@allthrive.ai>",
6
6
  "license": "MIT",
@@ -29,6 +29,7 @@
29
29
  "types": "dist/index.d.ts",
30
30
  "bin": {
31
31
  "agentic-loop": "./bin/agentic-loop.sh",
32
+ "ralph": "./bin/ralph.sh",
32
33
  "vibe-check": "./bin/vibe-check.js"
33
34
  },
34
35
  "exports": {