forgedock 1.0.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.
- package/LICENSE +244 -0
- package/README.md +251 -0
- package/bin/forgedock.mjs +232 -0
- package/commands/analytics.md +777 -0
- package/commands/audit-agents.md +410 -0
- package/commands/audit.md +410 -0
- package/commands/autopilot.md +432 -0
- package/commands/cleanup.md +264 -0
- package/commands/deploy-info.md +292 -0
- package/commands/failure-recon.md +375 -0
- package/commands/forge-stats.md +183 -0
- package/commands/geo-audit.md +343 -0
- package/commands/incident-response.md +384 -0
- package/commands/issue.md +506 -0
- package/commands/milestone.md +599 -0
- package/commands/orchestrate.md +1044 -0
- package/commands/pipeline-health.md +2343 -0
- package/commands/qa-sweep.md +237 -0
- package/commands/quality-gate.md +777 -0
- package/commands/review-pr-agents.md +1711 -0
- package/commands/review-pr-staging.md +325 -0
- package/commands/review-pr.md +888 -0
- package/commands/rollback.md +253 -0
- package/commands/security-audit.md +529 -0
- package/commands/sync-ecosystem.md +527 -0
- package/commands/validate.md +131 -0
- package/commands/work-on/build/architect.md +291 -0
- package/commands/work-on/build/context.md +349 -0
- package/commands/work-on/build/implement.md +280 -0
- package/commands/work-on/build/validate.md +292 -0
- package/commands/work-on/build.md +262 -0
- package/commands/work-on/close.md +350 -0
- package/commands/work-on/decompose.md +254 -0
- package/commands/work-on/investigate.md +437 -0
- package/commands/work-on/review.md +245 -0
- package/commands/work-on-monolithic.md +298 -0
- package/commands/work-on.md +1089 -0
- package/docs/CODEX.md +92 -0
- package/docs/WORKFLOW.md +104 -0
- package/package.json +45 -0
- package/scripts/verify-env-vars.sh +201 -0
- package/scripts/verify-host-headers.sh +117 -0
- package/scripts/verify-route-registration.sh +118 -0
- package/scripts/verify-sops-chain.sh +163 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Audit agent outputs from an orchestration run — timeline analysis, stall detection, active vs idle time breakdown
|
|
3
|
+
argument-hint: [session-id | latest | <agent-id>]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /audit-agents — Agent Output Auditor
|
|
7
|
+
|
|
8
|
+
**Input**: $ARGUMENTS
|
|
9
|
+
|
|
10
|
+
## Purpose
|
|
11
|
+
|
|
12
|
+
Parse agent JSONL output files from orchestration runs to produce actionable diagnostics:
|
|
13
|
+
- Per-agent phase timeline with durations
|
|
14
|
+
- Stall detection (gaps between phases caused by agent `end_turn` stops)
|
|
15
|
+
- Active vs idle time breakdown
|
|
16
|
+
- Resume cycle counting
|
|
17
|
+
- Wave-level efficiency metrics
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Phase 1: Locate Agent Outputs
|
|
22
|
+
|
|
23
|
+
### Step 1A: Find the session
|
|
24
|
+
|
|
25
|
+
Agent outputs live in `/tmp/claude-1000/` as symlinks to JSONL files in `~/.claude/projects/*/subagents/`.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# List recent sessions
|
|
29
|
+
ls -lt /tmp/claude-1000/ 2>/dev/null | head -10
|
|
30
|
+
|
|
31
|
+
# For each session dir, list agent output files
|
|
32
|
+
for DIR in /tmp/claude-1000/*/; do
|
|
33
|
+
TASKS_DIR="${DIR}*/tasks/"
|
|
34
|
+
AGENT_COUNT=$(find $TASKS_DIR -name "a*.output" -type l 2>/dev/null | wc -l)
|
|
35
|
+
if [ "$AGENT_COUNT" -gt 0 ]; then
|
|
36
|
+
echo "$(stat -c %Y "$DIR" 2>/dev/null) $DIR ($AGENT_COUNT agents)"
|
|
37
|
+
fi
|
|
38
|
+
done | sort -rn | head -5
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Input resolution:**
|
|
42
|
+
- `latest` or no argument → most recent session with agent outputs
|
|
43
|
+
- A session UUID → that specific session
|
|
44
|
+
- An agent ID (starts with `a`, 17+ hex chars) → find the session containing that agent
|
|
45
|
+
- A project path fragment (e.g., `ScraperAPI`) → filter to sessions for that project
|
|
46
|
+
|
|
47
|
+
### Step 1B: Collect agent JSONL files
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
SESSION_DIR="/tmp/claude-1000/{PROJECT_PATH}/{SESSION_ID}"
|
|
51
|
+
TASKS_DIR="${SESSION_DIR}/tasks"
|
|
52
|
+
|
|
53
|
+
# Agent files are symlinks starting with 'a' pointing to .jsonl files
|
|
54
|
+
for LINK in ${TASKS_DIR}/a*.output; do
|
|
55
|
+
AGENT_ID=$(basename "$LINK" .output)
|
|
56
|
+
TARGET=$(readlink -f "$LINK")
|
|
57
|
+
LINES=$(wc -l < "$TARGET" 2>/dev/null || echo 0)
|
|
58
|
+
echo "$AGENT_ID $LINES $TARGET"
|
|
59
|
+
done
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Only process files with `> 10` lines (smaller files are helper/polling agents, not work-on agents).
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Phase 2: Parse Each Agent
|
|
67
|
+
|
|
68
|
+
For each agent JSONL file, use a Python script to extract the timeline.
|
|
69
|
+
|
|
70
|
+
**IMPORTANT**: Run this as a SINGLE Python script, not per-agent bash loops. The JSONL files can be large (500+ lines, 100KB+).
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import json, sys, os
|
|
74
|
+
from datetime import datetime
|
|
75
|
+
from collections import defaultdict
|
|
76
|
+
|
|
77
|
+
def parse_agent(filepath, agent_id):
|
|
78
|
+
"""Parse a single agent JSONL file and return structured timeline data."""
|
|
79
|
+
with open(filepath) as f:
|
|
80
|
+
lines = f.readlines()
|
|
81
|
+
|
|
82
|
+
# Extract all events with timestamps
|
|
83
|
+
events = []
|
|
84
|
+
skill_invocations = [] # unique (ts, skill) pairs
|
|
85
|
+
skill_set = set()
|
|
86
|
+
tool_counts = defaultdict(int)
|
|
87
|
+
resume_count = 0
|
|
88
|
+
first_ts = last_ts = None
|
|
89
|
+
end_turn_points = []
|
|
90
|
+
|
|
91
|
+
for line in lines:
|
|
92
|
+
data = json.loads(line)
|
|
93
|
+
ts_str = data.get('timestamp', '')
|
|
94
|
+
if not ts_str:
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
ts = datetime.fromisoformat(ts_str.replace('Z', '+00:00'))
|
|
98
|
+
if first_ts is None:
|
|
99
|
+
first_ts = ts
|
|
100
|
+
last_ts = ts
|
|
101
|
+
|
|
102
|
+
msg = data.get('message', {})
|
|
103
|
+
stop_reason = msg.get('stop_reason', '')
|
|
104
|
+
content = msg.get('content', [])
|
|
105
|
+
|
|
106
|
+
if stop_reason == 'end_turn':
|
|
107
|
+
# Find the text content of this end_turn message
|
|
108
|
+
text = ''
|
|
109
|
+
if isinstance(content, list):
|
|
110
|
+
for c in content:
|
|
111
|
+
if c.get('type') == 'text':
|
|
112
|
+
text = c.get('text', '')[:100]
|
|
113
|
+
break
|
|
114
|
+
end_turn_points.append({'ts': ts, 'text': text})
|
|
115
|
+
|
|
116
|
+
if isinstance(content, list):
|
|
117
|
+
for c in content:
|
|
118
|
+
if c.get('type') == 'tool_use':
|
|
119
|
+
tool_name = c.get('name', '?')
|
|
120
|
+
tool_counts[tool_name] += 1
|
|
121
|
+
|
|
122
|
+
if tool_name == 'Skill':
|
|
123
|
+
inp = c.get('input', {})
|
|
124
|
+
skill_name = inp.get('skill', '?')
|
|
125
|
+
key = (ts_str, skill_name)
|
|
126
|
+
if key not in skill_set:
|
|
127
|
+
skill_set.add(key)
|
|
128
|
+
skill_invocations.append({
|
|
129
|
+
'ts': ts,
|
|
130
|
+
'skill': skill_name,
|
|
131
|
+
'args': inp.get('args', '')[:60]
|
|
132
|
+
})
|
|
133
|
+
else:
|
|
134
|
+
# Duplicate = resume replay
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
# Detect resume points: user messages that contain resume instructions
|
|
138
|
+
if data.get('type') == 'user':
|
|
139
|
+
if isinstance(content, str) and any(kw in content.lower() for kw in ['continue', 'resume', 'you need to']):
|
|
140
|
+
resume_count += 1
|
|
141
|
+
elif isinstance(content, list):
|
|
142
|
+
for c in content:
|
|
143
|
+
if c.get('type') == 'text':
|
|
144
|
+
t = c.get('text', '').lower()
|
|
145
|
+
if any(kw in t for kw in ['continue the', 'resume', 'you need to continue']):
|
|
146
|
+
resume_count += 1
|
|
147
|
+
|
|
148
|
+
# Count duplicate skill timestamps to detect resume replays
|
|
149
|
+
all_skill_timestamps = []
|
|
150
|
+
for line in lines:
|
|
151
|
+
data = json.loads(line)
|
|
152
|
+
msg = data.get('message', {})
|
|
153
|
+
content = msg.get('content', [])
|
|
154
|
+
if isinstance(content, list):
|
|
155
|
+
for c in content:
|
|
156
|
+
if c.get('type') == 'tool_use' and c.get('name') == 'Skill':
|
|
157
|
+
all_skill_timestamps.append(data.get('timestamp', ''))
|
|
158
|
+
|
|
159
|
+
# Duplicated timestamps indicate resume replays
|
|
160
|
+
ts_counts = defaultdict(int)
|
|
161
|
+
for t in all_skill_timestamps:
|
|
162
|
+
ts_counts[t] += 1
|
|
163
|
+
max_replays = max(ts_counts.values()) if ts_counts else 1
|
|
164
|
+
resume_cycles = max_replays - 1 # first occurrence is original, rest are replays
|
|
165
|
+
|
|
166
|
+
# Build phase timeline from unique skill invocations
|
|
167
|
+
phases = []
|
|
168
|
+
prev_ts = first_ts
|
|
169
|
+
for si in skill_invocations:
|
|
170
|
+
gap_sec = (si['ts'] - prev_ts).total_seconds()
|
|
171
|
+
phases.append({
|
|
172
|
+
'ts': si['ts'],
|
|
173
|
+
'skill': si['skill'],
|
|
174
|
+
'args': si['args'],
|
|
175
|
+
'gap_from_prev_sec': gap_sec,
|
|
176
|
+
'is_stall': gap_sec > 120 # > 2 min = stall
|
|
177
|
+
})
|
|
178
|
+
prev_ts = si['ts']
|
|
179
|
+
|
|
180
|
+
total_sec = (last_ts - first_ts).total_seconds() if first_ts and last_ts else 0
|
|
181
|
+
stall_sec = sum(p['gap_from_prev_sec'] for p in phases if p['is_stall'])
|
|
182
|
+
active_sec = total_sec - stall_sec
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
'agent_id': agent_id,
|
|
186
|
+
'filepath': filepath,
|
|
187
|
+
'jsonl_lines': len(lines),
|
|
188
|
+
'first_ts': first_ts,
|
|
189
|
+
'last_ts': last_ts,
|
|
190
|
+
'total_sec': total_sec,
|
|
191
|
+
'active_sec': active_sec,
|
|
192
|
+
'stall_sec': stall_sec,
|
|
193
|
+
'idle_pct': (stall_sec / total_sec * 100) if total_sec > 0 else 0,
|
|
194
|
+
'tool_counts': dict(tool_counts),
|
|
195
|
+
'phases': phases,
|
|
196
|
+
'end_turn_points': end_turn_points,
|
|
197
|
+
'resume_cycles': resume_cycles,
|
|
198
|
+
'skill_count': len(skill_invocations),
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Phase 3: Identify Issues
|
|
205
|
+
|
|
206
|
+
For each agent, try to determine which GitHub issue it was working on:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
# Read the first user message in the JSONL — it contains the agent prompt with issue number
|
|
210
|
+
python3 -c "
|
|
211
|
+
import json
|
|
212
|
+
with open('$FILEPATH') as f:
|
|
213
|
+
first = json.loads(f.readline())
|
|
214
|
+
msg = first.get('message', {}).get('content', '')
|
|
215
|
+
if isinstance(msg, list):
|
|
216
|
+
for c in msg:
|
|
217
|
+
if c.get('type') == 'text':
|
|
218
|
+
msg = c['text']
|
|
219
|
+
break
|
|
220
|
+
# Extract issue number
|
|
221
|
+
import re
|
|
222
|
+
m = re.search(r'#(\d+)', str(msg))
|
|
223
|
+
print(m.group(1) if m else 'unknown')
|
|
224
|
+
"
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Also extract the issue title from the prompt.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Phase 4: Generate Report
|
|
232
|
+
|
|
233
|
+
### Step 4A: Per-agent timeline
|
|
234
|
+
|
|
235
|
+
For each agent, display:
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
## Agent: #{ISSUE_NUMBER} — {ISSUE_TITLE}
|
|
239
|
+
**Duration**: {total_min} min (active: {active_min} min, idle: {stall_min} min — {idle_pct}% idle)
|
|
240
|
+
**Resume cycles**: {resume_cycles} (agent stopped and was resumed {resume_cycles} times)
|
|
241
|
+
**JSONL lines**: {lines} | **Tool calls**: Bash:{N} Read:{N} Edit:{N} Skill:{N}
|
|
242
|
+
|
|
243
|
+
### Phase Timeline
|
|
244
|
+
| Time | Phase | Duration | Gap | Status |
|
|
245
|
+
|------|-------|----------|-----|--------|
|
|
246
|
+
| 12:09:11 | work-on | — | — | start |
|
|
247
|
+
| 12:09:24 | investigate | 1m 3s | 13s | ok |
|
|
248
|
+
| 12:17:46 | build | — | **7m 22s** | STALL |
|
|
249
|
+
| 12:18:28 | build:context | 42s | 42s | ok |
|
|
250
|
+
| 12:42:13 | build:architect | 25s | **23m 45s** | STALL |
|
|
251
|
+
| 12:42:38 | build:implement | 39s | 25s | ok |
|
|
252
|
+
| 12:43:17 | build:validate | 4s | 39s | ok |
|
|
253
|
+
| 12:43:21 | quality-gate | 1m 18s | 4s | ok |
|
|
254
|
+
| 12:44:39 | review | 44s | 1m 18s | ok |
|
|
255
|
+
| 12:45:23 | review-pr | 2m 13s | 44s | ok |
|
|
256
|
+
| 12:47:36 | close | 1m 16s | 2m 13s | ok |
|
|
257
|
+
|
|
258
|
+
### end_turn Stops (caused stalls)
|
|
259
|
+
| Time | Last message before stop |
|
|
260
|
+
|------|--------------------------|
|
|
261
|
+
| 12:10:27 | `INVESTIGATE_RESULT: verdict: CONFIRMED...` |
|
|
262
|
+
| 12:19:07 | `Context phase complete. Returning to B4.` |
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Step 4B: Wave summary
|
|
266
|
+
|
|
267
|
+
```
|
|
268
|
+
## Wave Summary
|
|
269
|
+
|
|
270
|
+
| Agent | Issue | Total | Active | Idle | Idle% | Resumes | Stall Points |
|
|
271
|
+
|-------|-------|-------|--------|------|-------|---------|--------------|
|
|
272
|
+
| afbc… | #14513 | 40m | 8m | 31m | 80% | 2 | investigate→build, context→architect |
|
|
273
|
+
| a3b5… | #14508 | 23m | 23m | 0m | 0% | 0 | — |
|
|
274
|
+
| adf5… | #14514 | 55m | 12m | 43m | 78% | 3 | investigate→build, context→architect, implement→validate |
|
|
275
|
+
|
|
276
|
+
**Wave efficiency**: {avg_idle_pct}% idle time across all agents
|
|
277
|
+
**Longest stall**: {max_stall_min} min ({agent_id} between {phase_a} → {phase_b})
|
|
278
|
+
**Clean agents**: {N} of {total} ran without stalls
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Step 4C: Stall pattern analysis
|
|
282
|
+
|
|
283
|
+
Identify recurring stall patterns across agents:
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
## Stall Pattern Analysis
|
|
287
|
+
|
|
288
|
+
### Common stall boundaries
|
|
289
|
+
| Boundary | Agents affected | Avg gap |
|
|
290
|
+
|----------|----------------|---------|
|
|
291
|
+
| investigate → build | 4/5 | 7.5 min |
|
|
292
|
+
| context → architect | 4/5 | 23.8 min |
|
|
293
|
+
| implement → validate | 3/5 | 20.3 min |
|
|
294
|
+
|
|
295
|
+
### Root cause indicators
|
|
296
|
+
- **Synchronized stall times**: 4 agents stalled at 12:17, 12:42, 13:03
|
|
297
|
+
→ Orchestrator polling intervals, not agent-side issues
|
|
298
|
+
- **end_turn at phase boundaries**: Agent outputs result text then stops
|
|
299
|
+
→ LLM routing loop exits instead of continuing to next phase
|
|
300
|
+
- **Resume replays**: Each resume re-sends full conversation history
|
|
301
|
+
→ Context grows with each cycle, compounding the problem
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Step 4D: Recommendations
|
|
305
|
+
|
|
306
|
+
Based on the data, output specific recommendations:
|
|
307
|
+
|
|
308
|
+
- If idle% > 50% across wave → "Orchestrator polling too slow — agents spend more time waiting than working"
|
|
309
|
+
- If resume_cycles > 0 for most agents → "Routing loop in work-on.md not continuing past phase boundaries"
|
|
310
|
+
- If specific boundary stalls repeatedly → "Phase {X} returns text with end_turn instead of continuing loop — check work-on.md routing instructions"
|
|
311
|
+
- If one agent ran clean but others didn't → "Compare clean agent (#XXXX) vs stalled agents — what differs?"
|
|
312
|
+
|
|
313
|
+
### Step 4E: Persist summary (triggered by `--persist` flag)
|
|
314
|
+
|
|
315
|
+
**Trigger**: Run this step ONLY when `$ARGUMENTS` contains `--persist`. Default mode (no flag) skips this step and only prints to the conversation.
|
|
316
|
+
|
|
317
|
+
This step posts a structured `<!-- FORGE:AUDIT-AGENTS -->` summary comment to the Forge orchestration-metrics tracking issue so that `/pipeline-health` can query historical efficiency data.
|
|
318
|
+
|
|
319
|
+
**Step 4E.1 — Locate or create the tracking issue**:
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
# Ensure the label exists before using it (gh issue create fails with GraphQL error if label is absent)
|
|
323
|
+
gh label create "orchestration-metrics" -R RapierCraftStudios/forge \
|
|
324
|
+
--color "5319E7" --description "Running log of persisted audit-agents efficiency summaries" \
|
|
325
|
+
--force 2>/dev/null || true
|
|
326
|
+
|
|
327
|
+
TRACKING_ISSUE=$(gh issue list -R RapierCraftStudios/forge \
|
|
328
|
+
--state open --label "orchestration-metrics" --limit 1 \
|
|
329
|
+
--json number --jq '.[0].number' 2>/dev/null)
|
|
330
|
+
|
|
331
|
+
if [ -z "$TRACKING_ISSUE" ]; then
|
|
332
|
+
# Create the tracking issue on first use
|
|
333
|
+
TRACKING_ISSUE=$(gh issue create -R RapierCraftStudios/forge \
|
|
334
|
+
--title "Orchestration Metrics — Running Log" \
|
|
335
|
+
--label "orchestration-metrics" \
|
|
336
|
+
--body "This issue is a running log of persisted \`/audit-agents\` summaries. Each comment contains one session's efficiency metrics. Do not close this issue — \`/pipeline-health\` Phase 2K queries it to aggregate orchestration efficiency trends." \
|
|
337
|
+
--json number --jq '.number')
|
|
338
|
+
echo "Created orchestration-metrics tracking issue #$TRACKING_ISSUE"
|
|
339
|
+
fi
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
**Step 4E.2 — Compute wave-level aggregate metrics** (from the data already parsed in Phase 2):
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
# From the per-agent data computed in Phase 2, derive wave-level aggregates
|
|
346
|
+
TOTAL_AGENTS=$(echo "${AGENT_DATA[@]}" | jq 'length')
|
|
347
|
+
AVG_IDLE=$(echo "${AGENT_DATA[@]}" | jq '[.[].idle_pct] | add / length | . * 10 | round / 10')
|
|
348
|
+
AVG_RESUMES=$(echo "${AGENT_DATA[@]}" | jq '[.[].resume_cycles] | add / length | . * 100 | round / 100')
|
|
349
|
+
CLEAN_N=$(echo "${AGENT_DATA[@]}" | jq '[.[] | select(.idle_pct == 0 and .resume_cycles == 0)] | length')
|
|
350
|
+
|
|
351
|
+
# Top stall boundaries: aggregate gap_from_prev_sec > 120 entries by skill transition label
|
|
352
|
+
# Format: "investigate→build(4), context→architect(3), implement→validate(2)"
|
|
353
|
+
STALL_BOUNDARIES=$(echo "${AGENT_DATA[@]}" | jq -r '
|
|
354
|
+
[.[].phases[] | select(.is_stall) | .skill] |
|
|
355
|
+
group_by(.) | map({boundary: .[0], count: length}) |
|
|
356
|
+
sort_by(-.count) | .[:5] |
|
|
357
|
+
map("\(.boundary)(\(.count))") | join(", ")
|
|
358
|
+
')
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
**Step 4E.3 — Post the structured summary comment**:
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
SESSION_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
365
|
+
|
|
366
|
+
gh issue comment $TRACKING_ISSUE -R RapierCraftStudios/forge --body "<!-- FORGE:AUDIT-AGENTS -->
|
|
367
|
+
## Audit-Agents Summary — $SESSION_DATE
|
|
368
|
+
|
|
369
|
+
**Session**: \`$SESSION_ID\`
|
|
370
|
+
**Date**: $SESSION_DATE
|
|
371
|
+
**Agents**: $TOTAL_AGENTS
|
|
372
|
+
**Avg idle%**: $AVG_IDLE
|
|
373
|
+
**Avg resumes**: $AVG_RESUMES
|
|
374
|
+
**Clean agents**: $CLEAN_N/$TOTAL_AGENTS
|
|
375
|
+
**Stall boundaries**: $STALL_BOUNDARIES
|
|
376
|
+
|
|
377
|
+
_Posted by \`audit-agents --persist\`. Queried by \`/pipeline-health\` Phase 2K._
|
|
378
|
+
|
|
379
|
+
<!-- FORGE:AUDIT-AGENTS:COMPLETE -->"
|
|
380
|
+
|
|
381
|
+
echo "Persisted audit summary to tracking issue #$TRACKING_ISSUE"
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## Phase 5: Comparison Mode (optional)
|
|
387
|
+
|
|
388
|
+
If `$ARGUMENTS` contains `--compare` or two session IDs, run the analysis on both sessions and produce a diff:
|
|
389
|
+
|
|
390
|
+
```
|
|
391
|
+
## Session Comparison
|
|
392
|
+
|
|
393
|
+
| Metric | Session A | Session B | Delta |
|
|
394
|
+
|--------|-----------|-----------|-------|
|
|
395
|
+
| Avg agent duration | 38m | 24m | -37% |
|
|
396
|
+
| Avg idle% | 62% | 15% | -47pp |
|
|
397
|
+
| Avg resume cycles | 2.5 | 0.3 | -88% |
|
|
398
|
+
| Clean agents | 1/5 | 4/5 | +60pp |
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
This enables tracking whether orchestrator/prompt changes actually improved throughput.
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## Notes
|
|
406
|
+
|
|
407
|
+
- **File format**: Agent outputs are JSONL (one JSON object per line). Each line has `type` (user/assistant), `timestamp`, `message` (with `content` array and optional `stop_reason`).
|
|
408
|
+
- **Symlinks**: Files in `/tmp/claude-1000/*/tasks/a*.output` are symlinks to `~/.claude/projects/*/subagents/agent-*.jsonl`.
|
|
409
|
+
- **Size filtering**: Only analyze files > 10 lines. Small files are helper/polling agents spawned by the orchestrator for status checks.
|
|
410
|
+
- **Resume detection**: When an agent is resumed, the full conversation is replayed. Duplicate `(timestamp, skill_name)` pairs indicate replay cycles. `max(duplicates) - 1 = resume_cycles`.
|