chore: scaffold agent harness for Phase 2 eSignature envelope creation

Adds the ralph loop harness artifacts to drive autonomous implementation
of Phase 2. State lives in IMPLEMENTATION_PLAN.md so work survives
session resets — restart ralph-loop.sh after token refresh and the next
agent reads the plan to pick up where it left off.

Agent: human
Tests: N/A
Tests-Added: 0
TypeScript: N/A
This commit is contained in:
paulh 2026-04-09 21:34:11 -04:00
parent c49d127db1
commit b1c199d21d
5 changed files with 848 additions and 0 deletions

168
AGENT.md Normal file
View File

@ -0,0 +1,168 @@
# Agent Instructions — Salesforce Appraiser Review Letter
> Read this file at the start of every iteration. It is the canonical operating guide.
---
## Role
You are a senior Salesforce developer working autonomously on the appraiser review letter project.
You have full access to the codebase and can read, write, and run shell commands.
---
## Core Loop
### 1. Orient
- Read `PROJECT-SPEC.md` — requirements and acceptance criteria for Phase 2
- Read `IMPLEMENTATION_PLAN.md` — task list and current status
- Read `DECISIONS.md` — architecture decisions already in force
- Run `git log --oneline -10` — see what has been done
- Check for any prior escalations in `IMPLEMENTATION_PLAN.md`
### 2. Pick ONE Task
- Find the first unchecked task `- [ ]` in `IMPLEMENTATION_PLAN.md`
- If all tasks are checked, output `<promise>DONE</promise>` and exit
- Work only on this one task — do not pick up the next one
### 3. Implement
- Follow the existing code patterns in the project
- Read the relevant existing classes/LWCs before writing new code
- Study the CLM tracking pattern as the model for eSignature tracking
### 4. Verify — BLOCKING before commit
```bash
# Set up the CLI path (system Node v12 is too old — always use nvm Node)
NODE=/home/paulh/.nvm/versions/node/v22.22.0/bin/node
SF=/home/paulh/.nvm/versions/node/v22.22.0/lib/node_modules/@salesforce/cli/bin/run.js
# Deploy source to org
$NODE $SF project deploy start \
--source-dir force-app \
--target-org appraiser-dev
# Run Apex tests (synchronous, human-readable output)
$NODE $SF apex run test \
--target-org appraiser-dev \
--class-names DocusignESignatureServiceTest,CLMAdminServiceTest,AppraiserCasePayloadBuilderTest,CLMDocGenCalloutTest \
--result-format human \
--synchronous
```
**Do not commit if deploy fails or tests fail.**
For metadata-only tasks (adding custom fields, no Apex changes): deploy and confirm the object deploys cleanly, but you do not need to run all test classes — run a smoke test against the affected test class only.
### 5. Commit and Mark Done
Commit with this exact trailer format:
```
<type>(<scope>): <description>
<optional body>
Agent: claude-sonnet-4-6
Tests: N/N passing | N/A (metadata-only)
Tests-Added: +N | 0
TypeScript: N/A (Apex project)
```
Then mark the task `- [x]` in `IMPLEMENTATION_PLAN.md` and commit that update in the same commit.
### 6. Exit
Output a brief summary (what you did, test results, what is next) and exit.
The loop will restart you with fresh context for the next task.
---
## Rules
1. **One task per iteration.** Never work on multiple tasks.
2. **Read before writing.** Always read the existing class/LWC before modifying it.
3. **Follow existing patterns.** The CLM tracking pattern is the model for eSignature tracking.
4. **Tests are mandatory for every Apex change.** Add to the existing test classes — do not create new ones unless the spec says to.
5. **Never add fields or methods not required by the spec.** Implement what is asked, nothing more.
6. **Never modify production data.** All DML in tests uses `@isTest` and `Test.startTest()/stopTest()`.
7. **Mock HTTP callouts in tests.** Use `HttpCalloutMock` — same pattern as existing test classes.
8. **Deploy before committing.** Never commit code that does not deploy cleanly.
9. **Follow DECISIONS.md.** Do not change approaches listed as Accepted without escalating.
10. **If stuck, escalate.** Do not invent missing requirements.
---
## Escalation Protocol
Stop and add this block at the top of `IMPLEMENTATION_PLAN.md`, then output `<promise>STUCK</promise>`:
```markdown
## ESCALATION REQUIRED
- **Task:** [current task]
- **Issue:** [what is ambiguous/missing/conflicting]
- **What I need:** [specific question or decision]
- **What I'd do if I had to guess:** [best guess]
```
Escalate when:
- The spec has no FR covering a required decision
- A constraint in DECISIONS.md conflicts with the spec
- A Salesforce platform limit or API behavior is unclear
- A named credential or metadata record is missing from the org
- Any task requires deleting or overwriting existing custom fields already in use
---
## Output Signals
- `<promise>PLANNED</promise>` — plan created, ready for build
- `<promise>DONE</promise>` — all tasks complete
- `<promise>STUCK</promise>` — needs human intervention
- `<promise>ERROR</promise>` — unrecoverable error
---
## Project Structure
```
force-app/main/default/
├── classes/
│ ├── CLMAdminService.cls ← orchestration + persistDocGenResult pattern
│ ├── CLMAdminServiceTest.cls ← test pattern to follow
│ ├── CLMDocGenCallout.cls
│ ├── CLMDocGenCalloutTest.cls
│ ├── DocusignESignatureService.cls ← add createEnvelope() here
│ ├── DocusignESignatureServiceTest.cls
│ ├── AppraiserCasePayloadBuilder.cls
│ └── AppraiserCasePayloadBuilderTest.cls
├── lwc/
│ ├── clmDocGenWorkbench/ ← add Send for Signature button here
│ └── docusignEsignWorkbench/ ← read-only browsing (do not modify Phase 2)
└── objects/
└── Appraiser_Case__c/
└── fields/ ← add 5 eSignature tracking fields here
```
---
## SF CLI Invocation (Always Use This Pattern)
```bash
NODE=/home/paulh/.nvm/versions/node/v22.22.0/bin/node
SF=/home/paulh/.nvm/versions/node/v22.22.0/lib/node_modules/@salesforce/cli/bin/run.js
$NODE $SF <subcommand> <args>
```
Target org alias: `appraiser-dev`
---
## Context Anchor
- **Phase 1 is complete** — CLM doc generation works end-to-end. Do not modify it.
- **Phase 2 goal** — after a CLM doc is generated and attached, enable sending it for eSignature using a DocuSign template (Option A).
- **The authoritative plan** is in `NEXT_STEPS_DOCGEN.md` under Phase 2. `IMPLEMENTATION_PLAN.md` is the execution tracker derived from it.

122
DECISIONS.md Normal file
View File

@ -0,0 +1,122 @@
# Architecture Decision Records
> Non-obvious decisions that agents must not undo.
> Read during Orient phase every iteration.
---
### ADR-001: Option A (template-based envelope) for createEnvelope()
**Date:** 2026-04-09
**Status:** Accepted
**Context:**
Two approaches were considered for creating a DocuSign eSignature envelope:
- Option A: POST with a `templateId` — DocuSign template defines recipients/tabs, Salesforce only passes merge data at send time
- Option B: POST with a raw document (download CLM blob, upload as base64) — Salesforce manages all recipient config
**Decision:**
Use Option A. Simpler implementation, no blob download, recipient config lives in the DocuSign template where it belongs.
**Consequences:**
✅ Fewer lines of Apex (no ContentVersion download, no base64 encoding)
✅ Recipient/tab configuration lives in DocuSign where it is maintained
❌ Requires a matching eSignature template to exist in the DocuSign account
❌ If the template does not exist, the API returns a 400 — the operator must set up the template first
**Alternatives Considered:**
Option B was rejected as unnecessary complexity for this proof-of-concept phase.
---
### ADR-002: Named credentials for all DocuSign API calls
**Date:** 2026-04-09
**Status:** Accepted
**Context:**
All CLM and eSignature API calls must go through Salesforce named credentials. No hardcoded tokens, passwords, or base URLs in Apex.
**Decision:**
Named credential API name is always sourced from `CLM_Account_Setting__mdt`:
- CLM calls use `CLM_API_Named_Credential__c`
- eSignature calls use `ESignature_Rest_Named_Credential__c`
The `eSignatureAccountId` is also from `CLM_Account_Setting__mdt.ESignature_Account_Id__c`.
**Consequences:**
✅ No secrets in source code
✅ Works with Salesforce's credential management (OAuth token refresh, etc.)
❌ Named credentials must be manually configured in each org before the code works
---
### ADR-003: persistEnvelopeResult follows persistDocGenResult pattern
**Date:** 2026-04-09
**Status:** Accepted
**Context:**
The project already has `CLMAdminService.persistDocGenResult()` which writes CLM task results back to the case. The eSignature result persistence should follow the same pattern — not a new approach.
**Decision:**
`CLMAdminService.persistEnvelopeResult()` is an `@AuraEnabled` static method that takes structured parameters (not a blob or generic map) and writes specific fields to the case via a SOQL update. Same structure as `persistDocGenResult`.
**Consequences:**
✅ Consistent code pattern, agent can learn from existing method
✅ Testable with the same approach as `CLMAdminServiceTest`
---
### ADR-004: eSignature tracking fields belong on Appraiser_Case__c
**Date:** 2026-04-09
**Status:** Accepted
**Context:**
The case is the central record. CLM tracking fields already live on `Appraiser_Case__c`. The eSignature tracking fields follow the same pattern.
**Decision:**
All five eSignature lifecycle fields go on `Appraiser_Case__c`, not on a child record or a separate object.
**Consequences:**
✅ Single record for operator to view
✅ Consistent with existing CLM field placement
❌ Only one envelope tracked per case (sufficient for Phase 2)
---
### ADR-005: Operator manually enters template ID in Phase 2
**Date:** 2026-04-09
**Status:** Accepted
**Context:**
Option: auto-populate the template ID from `CLM_Letter_Definition__mdt` or present a template browser.
Both require additional metadata fields or another API call and are out of scope for Phase 2.
**Decision:**
The operator inputs the eSignature template ID directly into a text field in `clmDocGenWorkbench`. No auto-selection, no browser, no additional metadata field.
**Consequences:**
✅ Simpler Phase 2 scope
✅ No new metadata record changes needed
❌ Operator must know the template ID — acceptable for a proof-of-concept
**Review after:**
Phase 3 could add a `ESignature_Template_Id__c` field to `CLM_Letter_Definition__mdt` and pre-populate the input.
---
### ADR-006: HTTP mocks required for all Apex callout tests
**Date:** 2026-04-09
**Status:** Accepted
**Context:**
Salesforce requires all HTTP callouts in test classes to use `Test.setMock(HttpCalloutMock.class, ...)`. The existing test classes all follow this pattern.
**Decision:**
All tests for `createEnvelope()` must mock the HTTP response using an inner `MockHttpResponse` class implementing `HttpCalloutMock`. Do not attempt live callouts in test methods.
**Consequences:**
✅ Tests run in any Salesforce org without external connectivity
✅ Consistent with existing test class patterns (see `CLMDocGenCalloutTest`)
---
_Decisions don't drift if they're written down._

33
IMPLEMENTATION_PLAN.md Normal file
View File

@ -0,0 +1,33 @@
# Implementation Plan — Phase 2: eSignature Envelope Creation
> This is the live execution tracker. The agent reads and updates this every iteration.
> Check off tasks as they are completed (`- [x]`).
> Add `ESCALATION REQUIRED` block at the top if stuck.
---
## Tasks
- [ ] **Task 1 — Custom fields:** Add 5 eSignature tracking fields to `Appraiser_Case__c` object metadata and update `package.xml` if needed. Deploy to verify. (FR-001)
- [ ] **Task 2 — EnvelopeCreateResult class + createEnvelope() method:** Add `EnvelopeCreateResult` inner class and `createEnvelope(Id caseId, String accountCode, String templateId)` to `DocusignESignatureService`. (FR-002)
- [ ] **Task 3 — Tests for createEnvelope():** Add test coverage in `DocusignESignatureServiceTest` — success path (mock 201), failure path (mock 400), blank email guard. (NFR-001, TC-001, TC-002)
- [ ] **Task 4 — persistEnvelopeResult() + CaseContext extension:** Add `persistEnvelopeResult()` to `CLMAdminService`. Extend `CaseContext` inner class and `getCaseContext()` SOQL to include the 5 eSignature fields. (FR-003, FR-004)
- [ ] **Task 5 — Tests for persistEnvelopeResult():** Add test coverage in `CLMAdminServiceTest` — verify fields written correctly, `ESignature_Sent_At__c` is set, `ESignature_Completed_At__c` is NOT written on a "sent" result. (NFR-001, TC-003)
- [ ] **Task 6 — clmDocGenWorkbench UI:** Add "eSignature Template ID" text input and "Send for Signature" button to `clmDocGenWorkbench`. Button calls `createEnvelope()` then `persistEnvelopeResult()`, shows result. (FR-005)
- [ ] **Task 7 — Full deploy and test run:** Deploy entire change set, run all four test classes, verify all pass. Confirm no regressions. Update `docs/CURRENT_STATUS.md` to reflect Phase 2 completion.
---
## Completion
When all tasks are checked, output `<promise>DONE</promise>`.
---
_Phase 2 plan created: 2026-04-09_

327
PROJECT-SPEC.md Normal file
View File

@ -0,0 +1,327 @@
# Project Specification — Phase 2: eSignature Envelope Creation
---
## 1. Project Overview
### What are we building?
Connecting the CLM document generation workflow to DocuSign eSignature. After a review letter is generated and attached to an `Appraiser_Case__c` record, the operator can click "Send for Signature" to create a DocuSign envelope (using a pre-built eSignature template), send it, and have the envelope ID, status, and sent timestamp written back to the case.
### Why does it matter?
Phase 1 produces the review letter. Phase 2 delivers it for signature. Without this, the operator must manually log into DocuSign to send the envelope — a workflow gap.
### Success criteria
- [ ] Five eSignature tracking fields exist on `Appraiser_Case__c` and deploy cleanly
- [ ] `DocusignESignatureService.createEnvelope()` calls the eSignature REST API and returns a structured result
- [ ] Envelope ID, status, and sent timestamp are persisted to the case after a successful send
- [ ] `clmDocGenWorkbench` exposes a "Send for Signature" button that activates after attach succeeds
- [ ] All Apex changes are covered by tests in the existing test classes; all tests pass in appraiser-dev
---
## 2. Technical Foundation
### Tech stack
- **Language:** Apex (Salesforce), LWC (JavaScript/HTML)
- **Framework:** Salesforce DX / SFDX source format
- **Test framework:** Apex Test (deployed and run via sf CLI)
- **Package manager:** N/A — Salesforce metadata deployment
- **Org alias:** `appraiser-dev`
### Project structure
```
force-app/main/default/
├── classes/
│ ├── CLMAdminService.cls
│ ├── CLMAdminServiceTest.cls
│ ├── DocusignESignatureService.cls ← primary change target
│ └── DocusignESignatureServiceTest.cls ← test target
├── lwc/
│ └── clmDocGenWorkbench/ ← UI change target
└── objects/
└── Appraiser_Case__c/
└── fields/ ← 5 new fields
```
### Build and test commands
```bash
NODE=/home/paulh/.nvm/versions/node/v22.22.0/bin/node
SF=/home/paulh/.nvm/versions/node/v22.22.0/lib/node_modules/@salesforce/cli/bin/run.js
# Deploy
$NODE $SF project deploy start --source-dir force-app --target-org appraiser-dev
# Run tests
$NODE $SF apex run test \
--target-org appraiser-dev \
--class-names DocusignESignatureServiceTest,CLMAdminServiceTest \
--result-format human \
--synchronous
```
### Coding standards
- Follow existing Apex class structure — inner DTO classes, `@AuraEnabled` methods, `with sharing`
- Follow CLM tracking pattern: service layer method → result object → persist to case in `CLMAdminService`
- All HTTP callouts mocked in tests using `HttpCalloutMock` (see `CLMDocGenCalloutTest` for the pattern)
- LWC: follow existing `clmDocGenWorkbench` patterns (wire adapters, async/await handlers, tracked properties)
- Field API names use `__c` suffix; field-meta.xml follows Salesforce DX format
---
## 3. Requirements
### FR-001: eSignature Tracking Fields on Appraiser_Case__c
**Description:** Five custom fields to track the envelope lifecycle on the case record.
**Fields:**
- `ESignature_Envelope_Id__c` — Text(255), label "eSignature Envelope ID"
- `ESignature_Envelope_Status__c` — Text(50), label "eSignature Envelope Status" (values: created / sent / delivered / completed / voided)
- `ESignature_Sent_At__c` — DateTime, label "eSignature Sent At"
- `ESignature_Completed_At__c` — DateTime, label "eSignature Completed At"
- `ESignature_Envelope_Url__c` — URL(255), label "eSignature Envelope URL"
**Acceptance criteria:**
- [ ] All 5 field metadata XML files exist under `objects/Appraiser_Case__c/fields/`
- [ ] Fields deploy cleanly to appraiser-dev with no errors
- [ ] `package.xml` is updated to include the new fields (or already covers `CustomField` members)
---
### FR-002: createEnvelope() in DocusignESignatureService
**Description:** An Apex method that creates a DocuSign eSignature envelope using Option A (template-based). The caller provides a case ID, account code, and template ID. The method resolves the named credential from the account setting, calls `POST /v2.1/accounts/{accountId}/envelopes` with the template ID and a signer role populated from the case's appraiser fields, and returns a structured result.
**API call shape:**
```json
POST /v2.1/accounts/{accountId}/envelopes
{
"templateId": "<templateId>",
"templateRoles": [
{
"email": "<Appraiser_Email__c>",
"name": "<Appraiser_Name__c or Appraiser_Salutation__c + Appraiser_Last_Name__c>",
"roleName": "Signer"
}
],
"status": "sent"
}
```
**Result class:** `EnvelopeCreateResult` — inner class on `DocusignESignatureService`:
- `Boolean success`
- `String envelopeId`
- `String status`
- `String envelopeUri`
- `String errorMessage`
**Method signature:**
```apex
@AuraEnabled(cacheable=false)
public static EnvelopeCreateResult createEnvelope(
Id caseId,
String accountCode,
String templateId
)
```
**Acceptance criteria:**
- [ ] Method is `@AuraEnabled` and callable from LWC
- [ ] Resolves `eSignatureAccountId` and `eSignatureRestNamedCredential` from `CLM_Account_Setting__mdt` via `accountCode`
- [ ] Queries `Appraiser_Email__c`, `Appraiser_Name__c`, `Appraiser_Last_Name__c`, `Appraiser_Salutation__c` from the case
- [ ] POSTs correct JSON body to the eSignature API
- [ ] Returns `EnvelopeCreateResult` with `envelopeId` and `status` on success
- [ ] Returns `EnvelopeCreateResult` with `success=false` and `errorMessage` on failure (non-2xx or exception)
- [ ] Does NOT throw uncaught exceptions — catches and wraps in result
---
### FR-003: persistEnvelopeResult() in CLMAdminService
**Description:** After a successful `createEnvelope()` call, write the result back to `Appraiser_Case__c`. Follows the same pattern as `persistDocGenResult`.
**Method signature:**
```apex
@AuraEnabled(cacheable=false)
public static void persistEnvelopeResult(
Id caseId,
String envelopeId,
String envelopeStatus,
String envelopeUri
)
```
**Fields written:**
- `ESignature_Envelope_Id__c` = envelopeId
- `ESignature_Envelope_Status__c` = envelopeStatus
- `ESignature_Sent_At__c` = Datetime.now()
- `ESignature_Envelope_Url__c` = envelopeUri (if not blank)
**Acceptance criteria:**
- [ ] Method updates the correct case record (by `caseId`)
- [ ] Sets `ESignature_Sent_At__c` to current datetime
- [ ] Does not overwrite `ESignature_Completed_At__c` (only set when status = completed)
---
### FR-004: Extend CaseContext with Envelope Fields
**Description:** The `CaseContext` inner class in `CLMAdminService` (and its `getCaseContext()` query) must include the 5 eSignature tracking fields so the LWC can display current envelope status.
**Fields to add to CaseContext:**
- `String eSignatureEnvelopeId`
- `String eSignatureEnvelopeStatus`
- `String eSignatureEnvelopeUrl`
- `Datetime eSignatureSentAt`
- `Datetime eSignatureCompletedAt`
**Acceptance criteria:**
- [ ] `getCaseContext()` SOQL query includes all 5 fields
- [ ] `CaseContext` DTO maps all 5 fields with `@AuraEnabled`
---
### FR-005: "Send for Signature" Button in clmDocGenWorkbench
**Description:** After the "Attach Generated Document" step succeeds, a "Send for Signature" button becomes enabled. Clicking it calls `createEnvelope()` and then `persistEnvelopeResult()`, and displays the result (envelope ID and status, or error message) in the workbench UI.
**Behavior:**
- Button is disabled until `attachedFileContentDocumentId` is populated (i.e., attach has succeeded)
- Button shows a spinner while in progress (same pattern as generate/attach buttons)
- On success: display envelope ID and status; update envelope fields displayed from `caseContext`
- On error: display error message
- Button does NOT automatically select a template — the operator must provide `templateId` via a text input field in the UI
**New UI elements:**
- Text input: "eSignature Template ID" (maps to `templateId` parameter)
- Button: "Send for Signature"
- Status display: envelope ID, status, sent timestamp (read from refreshed `caseContext`)
**Acceptance criteria:**
- [ ] Button is disabled when `attachedFileContentDocumentId` is blank
- [ ] Button calls `DocusignESignatureService.createEnvelope()` with the case ID, account code, and template ID from the input
- [ ] On success, calls `CLMAdminService.persistEnvelopeResult()` and refreshes the case context display
- [ ] On failure, displays the error message from `EnvelopeCreateResult.errorMessage`
- [ ] Template ID input is cleared after a successful send
- [ ] Spinner shown during the callout; button disabled during in-flight request
---
### NFR-001: Test Coverage
- [ ] `DocusignESignatureServiceTest` covers `createEnvelope()` — at minimum: success path (mock HTTP 201), failure path (mock HTTP 400), missing template ID guard
- [ ] `CLMAdminServiceTest` covers `persistEnvelopeResult()` — at minimum: fields written correctly, `ESignature_Sent_At__c` is set
- [ ] All existing tests continue to pass after each change
### NFR-002: No Hardcoded Credentials
- [ ] All DocuSign API calls go through named credentials (never hardcoded tokens or passwords)
- [ ] Named credential API name is sourced from `CLM_Account_Setting__mdt`
---
## 4. Data Model
### New fields on Appraiser_Case__c
| API Name | Type | Length |
|----------|------|--------|
| ESignature_Envelope_Id__c | Text | 255 |
| ESignature_Envelope_Status__c | Text | 50 |
| ESignature_Sent_At__c | DateTime | — |
| ESignature_Completed_At__c | DateTime | — |
| ESignature_Envelope_Url__c | URL | 255 |
### Existing fields read by createEnvelope()
| API Name | Used for |
|----------|----------|
| Appraiser_Email__c | templateRoles[0].email |
| Appraiser_Name__c | templateRoles[0].name (prefer this) |
| Appraiser_Salutation__c | fallback for name |
| Appraiser_Last_Name__c | fallback for name |
---
## 5. Architecture Decisions
### Constraints
- **MUST:** Use `CLM_Account_Setting__mdt` to resolve named credentials — never hardcode
- **MUST:** Follow the `persistDocGenResult` pattern in `CLMAdminService` for `persistEnvelopeResult`
- **MUST:** Mock all HTTP callouts in tests using `HttpCalloutMock`
- **MUST NOT:** Modify the CLM document generation flow (Phase 1) — leave it untouched
- **MUST NOT:** Create new Apex test classes — add to existing ones
- **MUST NOT:** Add eSignature template browsing in this phase — the operator inputs the template ID directly
- **PREFER:** Option A (template-based envelope) for `createEnvelope()` — simpler, already decided
- **ESCALATE:** If the eSignature named credential does not exist in the org, stop and flag it
- **ESCALATE:** If `Appraiser_Email__c` is blank on the queried case, stop — do not silently send with a blank email
- **ESCALATE:** If the spec does not cover a required decision — do not fill gaps silently
### Dependencies
- `CLM_Account_Setting__mdt` — provides `ESignature_Account_Id__c`, `ESignature_Rest_Named_Credential__c`
- DocuSign eSignature REST API v2.1 — `/v2.1/accounts/{accountId}/envelopes`
- Named credential (AcctDemo_NamedCreds or equivalent) — must exist in appraiser-dev
### Known Challenges
- The named credential for eSignature REST may differ from the CLM named credential — check `CLM_Account_Setting__mdt` for `ESignature_Rest_Named_Credential__c`
- DocuSign template roles: the `roleName` must match the role name defined in the eSignature template — "Signer" is the default, but the actual template may use a different name. If tests fail with a 400 from DocuSign about role name, escalate.
- LWC must use `async/await` for imperative Apex calls (not wire adapters) for the send action — matches the existing attach pattern
### Rejected Approaches
| Rejected option | Why rejected |
|-----------------|-------------|
| Option B (upload CLM document as envelope document) | More complex, requires downloading the blob and managing recipients in code. Option A with a matching template is simpler and sufficient. |
| Creating a new Apex test class for eSignature | Not needed — `DocusignESignatureServiceTest` already exists and is the right home |
| Automatic template selection via API | Out of scope for Phase 2 — operator inputs template ID directly |
---
## 6. Reference Materials
### Existing code to learn from
- `CLMAdminService.persistDocGenResult()` — the exact pattern to replicate for `persistEnvelopeResult()`
- `CLMAdminService.getCaseContext()` — how case fields are queried and mapped to a DTO
- `CLMDocGenCalloutTest` — the HTTP mock pattern to follow in `DocusignESignatureServiceTest`
- `clmDocGenWorkbench` — the LWC pattern (async handlers, spinner, button enable/disable) to follow for the send button
### Anti-patterns
- Do not use `HttpRequest.setBodyAsBlob()` for the JSON envelope payload — use `setBody()` with `JSON.serialize()`
- Do not trust the `status` field in the LWC to determine if all prior steps completed — check the specific field (e.g., `attachedFileContentDocumentId`) that the button should gate on
---
## 7. Evaluation Design
### Test cases
#### TC-001: createEnvelope() success path
**Input:** Valid caseId, valid accountCode, valid templateId; mock HTTP returns 201 with `{"envelopeId":"abc-123","status":"sent","uri":"/envelopes/abc-123"}`
**Expected output:** `EnvelopeCreateResult.success = true`, `envelopeId = "abc-123"`, `status = "sent"`
**Verification:** `System.assertEquals(true, result.success)` in `DocusignESignatureServiceTest`
#### TC-002: createEnvelope() failure path
**Input:** Valid caseId/accountCode/templateId; mock HTTP returns 400 with error body
**Expected output:** `EnvelopeCreateResult.success = false`, `errorMessage` is non-blank
**Verification:** `System.assertEquals(false, result.success)` and `System.assertNotEquals(null, result.errorMessage)`
#### TC-003: persistEnvelopeResult() writes correct fields
**Input:** Existing case record, envelopeId = "test-env-id", envelopeStatus = "sent", envelopeUri = "/envelopes/test-env-id"
**Expected output:** Re-queried case has `ESignature_Envelope_Id__c = "test-env-id"`, `ESignature_Sent_At__c` is non-null
**Verification:** Re-query case after call and assert in `CLMAdminServiceTest`
---
_Last updated: 2026-04-09_

198
ralph-loop.sh Executable file
View File

@ -0,0 +1,198 @@
#!/usr/bin/env bash
#
# Ralph Wiggum Loop — Autonomous agent iteration
#
# Based on Geoffrey Huntley's approach:
# - Each iteration spawns a FRESH agent with clean context
# - Agent reads the plan, picks ONE task, implements, tests, commits, exits
# - Loop restarts until all tasks are done
#
# No context compaction. No stale reasoning. Just fresh starts.
#
# Usage:
# ./ralph-loop.sh # Build mode (default)
# ./ralph-loop.sh plan # Planning mode (create IMPLEMENTATION_PLAN.md)
# ./ralph-loop.sh --max 20 # Limit to 20 iterations
# ./ralph-loop.sh --agent claude # Use claude (default)
# ./ralph-loop.sh --agent codex # Use OpenAI Codex CLI
# ./ralph-loop.sh --agent aider # Use Aider
# ./ralph-loop.sh --agent gemini # Use Gemini CLI
# ./ralph-loop.sh --agent custom # Use custom agent (see below)
#
# Extensibility:
# To add support for other AI coding agents (aider, cursor, windsurf, etc.):
# 1. Add a new case in the run_agent() function's agent selection block
# 2. Format the prompt appropriately for that agent's CLI interface
# 3. Ensure the agent outputs to the logfile for promise detection
#
# Example for Aider:
# aider)
# aider --message "$prompt" --yes 2>&1 | tee "$logfile"
# ;;
#
# Example for custom script:
# custom)
# ./my-agent-wrapper.sh "$prompt" 2>&1 | tee "$logfile"
# ;;
#
set -euo pipefail
MODE="${1:-build}"
MAX_ITERATIONS=50
AGENT="claude"
PLAN_FILE="IMPLEMENTATION_PLAN.md"
SPEC_FILE="PROJECT-SPEC.md"
AGENT_FILE="AGENT.md"
LOG_DIR=".ralph-logs"
# Parse arguments
shift 2>/dev/null || true
while [[ $# -gt 0 ]]; do
case "$1" in
--max) MAX_ITERATIONS="$2"; shift 2 ;;
--agent) AGENT="$2"; shift 2 ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
mkdir -p "$LOG_DIR"
# Colors
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
BLUE='\033[0;34m'
NC='\033[0m'
log() { echo -e "${BLUE}[ralph]${NC} $1"; }
success() { echo -e "${GREEN}[ralph]${NC} $1"; }
warn() { echo -e "${YELLOW}[ralph]${NC} $1"; }
error() { echo -e "${RED}[ralph]${NC} $1"; }
# Check prerequisites
if [[ ! -f "$SPEC_FILE" ]]; then
error "Missing $SPEC_FILE — create your project spec first."
exit 1
fi
if [[ ! -f "$AGENT_FILE" ]]; then
warn "No $AGENT_FILE found. Using default agent instructions."
fi
run_agent() {
local iteration=$1
local mode=$2
local logfile="$LOG_DIR/iteration-${iteration}.log"
local prompt=""
if [[ "$mode" == "plan" ]]; then
prompt="Read PROJECT-SPEC.md. Decompose the project into discrete, testable tasks ordered by dependency. Write the plan to IMPLEMENTATION_PLAN.md with checkboxes. Output <promise>PLANNED</promise> when done."
else
prompt="Read AGENT.md (if it exists) for your instructions. Follow the core loop: orient, pick one task, implement, verify, commit, exit."
fi
log "Iteration $iteration ($mode mode) — starting fresh agent..."
# Agent selection block
# Extend this case statement to support additional agents
case "$AGENT" in
claude)
echo "$prompt" | claude -p --output-format text 2>&1 | tee "$logfile"
;;
codex)
echo "$prompt" | codex 2>&1 | tee "$logfile"
;;
aider)
# Aider: AI pair programming in your terminal
# https://aider.chat
aider --message "$prompt" --yes 2>&1 | tee "$logfile"
;;
gemini)
# Google Gemini CLI (if available)
# Adjust command based on actual Gemini CLI interface
echo "$prompt" | gemini-cli 2>&1 | tee "$logfile"
;;
custom)
# Custom agent integration
# Replace this with your own agent wrapper script
# The script should:
# 1. Accept prompt as first argument or via stdin
# 2. Perform the requested work (read files, write code, run tests, commit)
# 3. Output promise signals: <promise>PLANNED|DONE|STUCK|ERROR</promise>
# 4. Exit with appropriate code
if [[ -x "./custom-agent.sh" ]]; then
./custom-agent.sh "$prompt" 2>&1 | tee "$logfile"
else
error "Custom agent selected but ./custom-agent.sh not found or not executable"
exit 1
fi
;;
*)
error "Unknown agent: $AGENT"
error "Supported agents: claude, codex, aider, gemini, custom"
error "To add support for other agents, edit the run_agent() function in this script"
exit 1
;;
esac
return 0
}
check_output() {
local logfile="$1"
if grep -q '<promise>DONE</promise>' "$logfile" 2>/dev/null; then
return 0 # Done
elif grep -q '<promise>STUCK</promise>' "$logfile" 2>/dev/null; then
return 2 # Stuck
elif grep -q '<promise>ERROR</promise>' "$logfile" 2>/dev/null; then
return 3 # Error
else
return 1 # Continue
fi
}
# Main loop
if [[ "$MODE" == "plan" ]]; then
log "Planning mode — creating implementation plan..."
run_agent 0 plan
success "Plan created. Review $PLAN_FILE, then run: ./ralph-loop.sh"
exit 0
fi
log "Starting Ralph Wiggum loop (max $MAX_ITERATIONS iterations)"
log "Agent: $AGENT"
log "Spec: $SPEC_FILE"
log "Plan: $PLAN_FILE"
echo ""
for i in $(seq 1 "$MAX_ITERATIONS"); do
run_agent "$i" build
logfile="$LOG_DIR/iteration-${i}.log"
check_output "$logfile"
status=$?
case $status in
0)
success "🎉 ALL TASKS COMPLETE after $i iterations!"
exit 0
;;
2)
warn "Agent is stuck. Review $logfile and intervene."
exit 1
;;
3)
error "Agent encountered an error. Review $logfile."
exit 1
;;
1)
log "Iteration $i complete. Restarting with fresh context..."
echo ""
sleep 2
;;
esac
done
warn "Reached max iterations ($MAX_ITERATIONS). Review progress in $PLAN_FILE."
exit 1