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:
parent
c49d127db1
commit
b1c199d21d
|
|
@ -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.
|
||||
|
|
@ -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._
|
||||
|
|
@ -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_
|
||||
|
|
@ -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_
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue