CI fails, you fix it, it seems to work, then another problem pops up — a fairly typical late-night debugging session. This is a record of one such session.
What made this one worth writing about was seeing how "a single missing \ caused three errors to cascade," along with a design decision about how to stabilize AI agent output.
The Trigger: The rewrite-issue Job Stopped Working
I'm developing a tool called DevLoop Runner. It automates development workflows using AI agents, and one of its features is a rewrite-issue command that automatically redesigns GitHub Issues. This runs as a Jenkins pipeline job, and one day it stopped working.
The job failed with this error:
hudson.AbortException: Attempted to execute a step that requires a node context
while 'agent none' was specified.
On top of that, a NullPointerException was appearing in the post block. Three different types of errors in the logs — honestly, I hesitated for a moment about where to start.
Problem 1: The Groovy Variable Resolution Timing Trap
Reading the Error
Following the stack trace, the first thing that caught my eye was the node context error. But that wasn't the real cause. At the end of the log, there was another error — subtle but decisive:
groovy.lang.MissingPropertyException: No such property: WORKSPACE
for class: groovy.lang.Binding
WORKSPACE not found. If you've worked with Jenkins, you might think "Wait, isn't WORKSPACE a built-in variable?" I thought so too.
Root Cause: A Missing $ Escape
Comparing it with another working Jenkinsfile (auto-issue), there was only one difference:
// ❌ rewrite-issue (the broken one)
args "-v ${WORKSPACE}:/workspace -w /workspace"
// ✅ auto-issue (the working one)
args "-v \${WORKSPACE}:/workspace -w /workspace"
One missing \. That's it.
But the implications run deep. In Declarative Pipeline, the agent block definition is parsed by Groovy before node allocation. This means if you don't escape ${WORKSPACE}, Groovy tries to resolve the variable at a stage when no node exists yet, resulting in MissingPropertyException.
The Cascading Failure Structure
This is the key point. Three errors cascaded from a single cause:
graph TD
A["Missing ${WORKSPACE} escape"] --> B["MissingPropertyException during Groovy parsing"]
B --> C["Node allocation never happens"]
C --> D["Post block executes without node context"]
D --> E["sh / cleanWs() throws node context required error"]
C --> F["Shared library variables never initialized"]
F --> G["common.sendWebhook() throws NullPointerException"]
All three errors in the logs traced back to this single character.
The fix was literally one line:
- args "-v ${WORKSPACE}:/workspace -w /workspace -e CLAUDE_DANGEROUSLY_SKIP_PERMISSIONS=1"
+ args "-v \${WORKSPACE}:/workspace -w /workspace -e CLAUDE_DANGEROUSLY_SKIP_PERMISSIONS=1"
The lesson here: in Jenkins error logs, the first error to appear isn't necessarily the root cause. Often, the root cause is buried near the end. In this case, MissingPropertyException appeared at the very end of the log — if I had only chased the node context error that appeared first, I would have gone in circles.
Problem 2: How to Stabilize AI Agent Output
After fixing the Jenkinsfile and re-running the job, it succeeded this time. The log even showed Successfully updated issue #682.
But when I checked the actual GitHub Issue — it hadn't been updated.
Why "Success" Didn't Mean Updated
Looking at the log again, there was a WARNING:
WARNING: Failed to parse agent response as JSON:
Unexpected non-whitespace character after JSON at position 740
Re-reading the rewrite-issue.ts implementation revealed the problem.
When asking the AI agent (Claude Code) to redesign an Issue, it responds with text. To extract JSON from that text, the code used a regex — specifically /\{[\s\S]*\}/, a greedy match.
The agent's response can include explanatory text and code blocks alongside JSON. When there are multiple { and } characters, the regex grabs everything from the first { to the last }. Naturally, that isn't valid JSON.
The bigger problem was the fallback handling when JSON parsing failed. On failure, it returned the original title (fallbackTitle) and the agent's raw response as the "new body." As a result, the GitHub API's updateIssue would "succeed," but the update was essentially meaningless (or the title was identical, so no diff was detected).
// Fallback handling (before fix)
return {
newTitle: fallbackTitle, // ← Original title returned as-is
newBody: response.trim(), // ← Agent's raw response in full
};
Design Decision: From Text Extraction to File Output
There were several possible fixes: making the regex non-greedy (/\{[\s\S]*?\}/), writing a proper JSON block parser, etc.
But honestly, what I realized was that the approach of "extracting JSON from agent text output" itself was inherently unstable. The agent's response format varies subtly each time, and even if I fixed this case, another pattern could break it again.
So I decided to fundamentally change the approach. Instead of having the agent "output" JSON, have it "write" to a specified file path.
graph LR
subgraph "Before"
A1[Agent] -->|Text response| B1[Regex extraction]
B1 -->|Unstable| C1[JSON.parse]
end
subgraph "After"
A2[Agent] -->|File write| B2[File read]
B2 -->|Stable| C2[JSON.parse]
end
Specifically, the output file path is embedded in the prompt, and the agent uses its Write tool to save a JSON file. The command handler simply reads that file.
Here's what the prompt change looked like:
## Output Format
- Please output in the following JSON format:
+ Please **write to the specified file path** in the following JSON format:
+
+ **Output file path**: `{OUTPUT_FILE_PATH}`
```json
{
"title": "New title (80 characters or less)",
"body": "New body (Markdown format)",
...
}
- Important: You must write the JSON file to the absolute path above using the Write tool.
- Saving as a file is required, not console output.
On the command handler side, a temporary file path is generated and injected into the prompt, then read after agent execution:
```typescript
// Temporary file path generation
function createOutputFilePath(repoPath: string, issueNumber?: number): string {
const suffix = issueNumber ? `-${issueNumber}` : '';
const filename = `rewrite-issue${suffix}-${Date.now()}.json`;
const tmpDir = path.join(repoPath, '.ai-workflow', 'tmp');
if (!fs.existsSync(tmpDir)) {
fs.mkdirSync(tmpDir, { recursive: true });
}
return path.join(tmpDir, filename);
}
The legacy text extraction is kept as a fallback in case the file isn't found, but the greedy match bug has been fixed and the extractor rewritten to track bracket depth.
Bonus: The Double-Escape Bug
While I was at it, I found a double-escape bug in generateUnifiedDiff and calculateDefaultMetrics:
// Before (extra backslash escape in string literal)
const oldLines = oldBody.split('\\n'); // Splits by literal "\n" (two chars)
const sectionCount = (body.match(/^##\\s+/gm) ?? []).length; // Won't match
// After
const oldLines = oldBody.split('\n'); // Correctly splits by newline
const sectionCount = (body.match(/^##\s+/gm) ?? []).length; // Correctly matches
This is an easy trap to fall into when you forget that escape handling differs between TypeScript string literals and regex literals. '\\n' is not a newline character — it's a two-character literal: backslash + n.
Retrospective
What both problems had in common was that "escaping" — a mundane detail — was breaking the entire system.
In the Jenkinsfile, a single \ triggered a cascading failure through Groovy variable resolution, node allocation, and post-block processing. In TypeScript, double-escaping \\ broke string splitting and regex matching.
Both are the insidious type of bug that "seems to work" until it actually runs and the problem surfaces.
As for the AI agent output approach, switching from text extraction to file output has been the right call so far. By removing dependency on the agent's response format, parsing stability improved dramatically. That said, whether the agent actually writes to the file depends on the agent's behavior, so the fallback remains necessary. DevLoop Runner is a tool that "has AI write code," so these seemingly mundane design decisions about "how to receive AI output" directly impact stability. I'd like to run it a bit longer before making a final judgment.
Recommended Reading
For those who want to systematically learn about Jenkins pipeline design and troubleshooting, this book is a great reference.
[📦 商品リンク: moshimo-book-e7vQm]
For practical insights on integrating AI agents into development workflows, this is a useful resource.
[📦 商品リンク: moshimo-book-9CeFY]
For a broader understanding of AI agent design and operations.
[📦 商品リンク: moshimo-card-tpujl]